@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,181 @@
|
|
|
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
|
+
export type Locale = {
|
|
6
|
+
/**
|
|
7
|
+
* An [IETF BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) with a region code.
|
|
8
|
+
* @example
|
|
9
|
+
* `'en-US'`, `'es-419'`, `'pl-PL'`.
|
|
10
|
+
*/
|
|
11
|
+
languageTag: string;
|
|
12
|
+
/**
|
|
13
|
+
* An [IETF BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) without the region code.
|
|
14
|
+
* @example
|
|
15
|
+
* `'en'`, `'es'`, `'pl'`.
|
|
16
|
+
*/
|
|
17
|
+
languageCode: string | null;
|
|
18
|
+
/**
|
|
19
|
+
* 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.
|
|
20
|
+
* @example
|
|
21
|
+
* `'Latn'`, `'Hans'`, `'Hebr'`.
|
|
22
|
+
*/
|
|
23
|
+
languageScriptCode: string | null;
|
|
24
|
+
/**
|
|
25
|
+
* 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).
|
|
26
|
+
* @example
|
|
27
|
+
* `'US'`.
|
|
28
|
+
*/
|
|
29
|
+
regionCode: string | null;
|
|
30
|
+
/**
|
|
31
|
+
* 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`).
|
|
32
|
+
* Prefer using `regionCode` for any internalization purposes.
|
|
33
|
+
* @example
|
|
34
|
+
* `'US'`.
|
|
35
|
+
*/
|
|
36
|
+
languageRegionCode: string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Currency code for the locale.
|
|
39
|
+
* On iOS, it's the currency code from the `Region` setting under Language & Region, not for the current locale.
|
|
40
|
+
* On Android, it's the currency specifc to the locale in the list, as there are no separate settings for selecting a region.
|
|
41
|
+
* Is `null` on Web, use a table lookup based on region instead.
|
|
42
|
+
* @example
|
|
43
|
+
* `'USD'`, `'EUR'`, `'PLN'`.
|
|
44
|
+
*/
|
|
45
|
+
currencyCode: string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Currency symbol for the currency specified by `currencyCode`.
|
|
48
|
+
* @example
|
|
49
|
+
* `'$'`, `'€'`, `'zł'`.
|
|
50
|
+
*/
|
|
51
|
+
currencySymbol: string | null;
|
|
52
|
+
/**
|
|
53
|
+
* Currency code for the locale.
|
|
54
|
+
* On iOS, it's the currency code for the current locale in the list, not the device region.
|
|
55
|
+
* On Android, it's equal to `currencyCode`.
|
|
56
|
+
* Is `null` on Web.
|
|
57
|
+
* Prefer using `currencyCode` for any internalization purposes.
|
|
58
|
+
* @example
|
|
59
|
+
* `'USD'`, `'EUR'`, `'PLN'`.
|
|
60
|
+
*/
|
|
61
|
+
languageCurrencyCode: string | null;
|
|
62
|
+
/**
|
|
63
|
+
* Currency symbol for the currency specified by `languageCurrencyCode`.
|
|
64
|
+
* Prefer using `currencySymbol` for any internalization purposes.
|
|
65
|
+
* @example
|
|
66
|
+
* `'$'`, `'€'`, `'zł'`.
|
|
67
|
+
*/
|
|
68
|
+
languageCurrencySymbol: string | null;
|
|
69
|
+
/**
|
|
70
|
+
* Decimal separator used for formatting numbers with fractional parts.
|
|
71
|
+
* @example
|
|
72
|
+
* `'.'`, `','`.
|
|
73
|
+
*/
|
|
74
|
+
decimalSeparator: string | null;
|
|
75
|
+
/**
|
|
76
|
+
* Digit grouping separator used for formatting large numbers.
|
|
77
|
+
* @example
|
|
78
|
+
* `'.'`, `','`.
|
|
79
|
+
*/
|
|
80
|
+
digitGroupingSeparator: string | null;
|
|
81
|
+
/**
|
|
82
|
+
* Text direction for the locale. One of: `'ltr'`, `'rtl'`.
|
|
83
|
+
*/
|
|
84
|
+
textDirection: 'ltr' | 'rtl';
|
|
85
|
+
/**
|
|
86
|
+
* The measurement system used in the locale.
|
|
87
|
+
* Is `null` on Web, as user chosen measurement system is not exposed on the web and using locale to determine measurement systems is unreliable.
|
|
88
|
+
* Ask for user preferences if possible.
|
|
89
|
+
*/
|
|
90
|
+
measurementSystem: `metric` | `us` | `uk` | null;
|
|
91
|
+
/**
|
|
92
|
+
* The temperature unit used in the locale.
|
|
93
|
+
* Returns `null` if the region code is unknown.
|
|
94
|
+
*/
|
|
95
|
+
temperatureUnit: 'celsius' | 'fahrenheit' | null;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* An enum mapping days of the week in Gregorian calendar to their index as returned by the `firstWeekday` property.
|
|
100
|
+
*/
|
|
101
|
+
export enum Weekday {
|
|
102
|
+
SUNDAY = 1,
|
|
103
|
+
MONDAY = 2,
|
|
104
|
+
TUESDAY = 3,
|
|
105
|
+
WEDNESDAY = 4,
|
|
106
|
+
THURSDAY = 5,
|
|
107
|
+
FRIDAY = 6,
|
|
108
|
+
SATURDAY = 7,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The calendar identifier, one of [Unicode calendar types](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar).
|
|
113
|
+
*/
|
|
114
|
+
export enum CalendarIdentifier {
|
|
115
|
+
/** Thai Buddhist calendar */
|
|
116
|
+
BUDDHIST = 'buddhist',
|
|
117
|
+
/** Traditional Chinese calendar */
|
|
118
|
+
CHINESE = 'chinese',
|
|
119
|
+
/** Coptic calendar */
|
|
120
|
+
COPTIC = 'coptic',
|
|
121
|
+
/** Traditional Korean calendar */
|
|
122
|
+
DANGI = 'dangi',
|
|
123
|
+
/** Ethiopic calendar, Amete Alem (epoch approx. 5493 B.C.E) */
|
|
124
|
+
ETHIOAA = 'ethioaa',
|
|
125
|
+
/** Ethiopic calendar, Amete Mihret (epoch approx, 8 C.E.) */
|
|
126
|
+
ETHIOPIC = 'ethiopic',
|
|
127
|
+
/** Gregorian calendar (also referred to upstream as "gregorian" — dropped as a separate
|
|
128
|
+
* member here since this repo's lint bans duplicate enum values). */
|
|
129
|
+
GREGORY = 'gregory',
|
|
130
|
+
/** Traditional Hebrew calendar */
|
|
131
|
+
HEBREW = 'hebrew',
|
|
132
|
+
/** Indian calendar */
|
|
133
|
+
INDIAN = 'indian',
|
|
134
|
+
/** Islamic calendar */
|
|
135
|
+
ISLAMIC = 'islamic',
|
|
136
|
+
/** Islamic calendar, tabular (intercalary years [2,5,7,10,13,16,18,21,24,26,29] - civil epoch) */
|
|
137
|
+
ISLAMIC_CIVIL = 'islamic-civil',
|
|
138
|
+
/** Islamic calendar, Saudi Arabia sighting */
|
|
139
|
+
ISLAMIC_RGSA = 'islamic-rgsa',
|
|
140
|
+
/**Islamic calendar, tabular (intercalary years [2,5,7,10,13,16,18,21,24,26,29] - astronomical epoch) */
|
|
141
|
+
ISLAMIC_TBLA = 'islamic-tbla',
|
|
142
|
+
/** Islamic calendar, Umm al-Qura */
|
|
143
|
+
ISLAMIC_UMALQURA = 'islamic-umalqura',
|
|
144
|
+
/** ISO calendar (Gregorian calendar using the ISO 8601 calendar week rules) */
|
|
145
|
+
ISO8601 = 'iso8601',
|
|
146
|
+
/** Japanese imperial calendar */
|
|
147
|
+
JAPANESE = 'japanese',
|
|
148
|
+
/** Persian calendar */
|
|
149
|
+
PERSIAN = 'persian',
|
|
150
|
+
/** Civil (algorithmic) Arabic calendar */
|
|
151
|
+
ROC = 'roc',
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type Calendar = {
|
|
155
|
+
/**
|
|
156
|
+
* The calendar identifier, one of [Unicode calendar types](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar).
|
|
157
|
+
*
|
|
158
|
+
* On Android is limited to one of device's [available calendar types](https://developer.android.com/reference/java/util/Calendar#getAvailableCalendarTypes()).
|
|
159
|
+
*
|
|
160
|
+
* 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.
|
|
161
|
+
*/
|
|
162
|
+
calendar: CalendarIdentifier | null;
|
|
163
|
+
/**
|
|
164
|
+
* True when current device settings use 24-hour time format.
|
|
165
|
+
* 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.
|
|
166
|
+
*/
|
|
167
|
+
uses24hourClock: boolean | null;
|
|
168
|
+
/**
|
|
169
|
+
* The first day of the week. For most calendars Sunday is numbered `1`, with Saturday being number `7`.
|
|
170
|
+
* 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.
|
|
171
|
+
* @example
|
|
172
|
+
* `1`, `7`.
|
|
173
|
+
*/
|
|
174
|
+
firstWeekday: Weekday | null;
|
|
175
|
+
/**
|
|
176
|
+
* Time zone for the calendar. Can be `null` on Web.
|
|
177
|
+
* @example
|
|
178
|
+
* `'America/Los_Angeles'`, `'Europe/Warsaw'`, `'GMT+1'`.
|
|
179
|
+
*/
|
|
180
|
+
timeZone: string | null;
|
|
181
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// React lifecycle wiring over the framework-agnostic core — mirrors use-locales' shape exactly.
|
|
2
|
+
// Ported from expo-localization's own useCalendars hook (sdk-57).
|
|
3
|
+
import { useEffect, useMemo, useReducer } from 'react';
|
|
4
|
+
import { addCalendarListener, getCalendars, type Calendar } from '../../../core';
|
|
5
|
+
|
|
6
|
+
export function useCalendars(): Calendar[] {
|
|
7
|
+
const [invalidationKey, invalidate] = useReducer((key: number) => key + 1, 0);
|
|
8
|
+
const calendars = useMemo(() => getCalendars(), [invalidationKey]);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const subscription = addCalendarListener(invalidate);
|
|
12
|
+
return () => subscription.remove();
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
15
|
+
return calendars;
|
|
16
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Co-located React-driven test (ADR 0025) for useCalendars. See use-locales' test for the shared
|
|
2
|
+
// rationale (mocks `core`, not expo-modules-core internals).
|
|
3
|
+
|
|
4
|
+
import { createElement, type ReactElement } from 'react';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { mount, unmount, View } from '@symbiote-native/react';
|
|
7
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
8
|
+
import { CalendarIdentifier, Weekday, type Calendar } from '../../../core/types';
|
|
9
|
+
import { useCalendars } from './index';
|
|
10
|
+
|
|
11
|
+
const FAKE_CALENDARS_INITIAL: Calendar[] = [
|
|
12
|
+
{
|
|
13
|
+
calendar: CalendarIdentifier.GREGORY,
|
|
14
|
+
uses24hourClock: true,
|
|
15
|
+
firstWeekday: Weekday.SUNDAY,
|
|
16
|
+
timeZone: 'Europe/Warsaw',
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const FAKE_CALENDARS_UPDATED: Calendar[] = [
|
|
21
|
+
{
|
|
22
|
+
calendar: CalendarIdentifier.ISO8601,
|
|
23
|
+
uses24hourClock: false,
|
|
24
|
+
firstWeekday: Weekday.MONDAY,
|
|
25
|
+
timeZone: 'America/New_York',
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const { addListener, getCalendars, remove } = vi.hoisted(() => {
|
|
30
|
+
const remove = vi.fn();
|
|
31
|
+
return {
|
|
32
|
+
addListener: vi.fn((_listener: () => void) => ({ remove })),
|
|
33
|
+
getCalendars: vi.fn(),
|
|
34
|
+
remove,
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
vi.mock('../../../core', () => ({
|
|
39
|
+
addCalendarListener: addListener,
|
|
40
|
+
getCalendars,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
const ROOT_TAG = 955;
|
|
44
|
+
|
|
45
|
+
const results: Calendar[][] = [];
|
|
46
|
+
|
|
47
|
+
function Probe(): ReactElement {
|
|
48
|
+
results.push(useCalendars());
|
|
49
|
+
return createElement(View);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const fabric = installFabric();
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
fabric.reset();
|
|
56
|
+
results.length = 0;
|
|
57
|
+
vi.clearAllMocks();
|
|
58
|
+
getCalendars.mockReturnValue(FAKE_CALENDARS_INITIAL);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterEach(() => unmount(ROOT_TAG));
|
|
62
|
+
|
|
63
|
+
describe('useCalendars', () => {
|
|
64
|
+
it('reads the current calendars synchronously on first render', () => {
|
|
65
|
+
mount(ROOT_TAG, createElement(Probe));
|
|
66
|
+
|
|
67
|
+
expect(results[results.length - 1]).toEqual(FAKE_CALENDARS_INITIAL);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('recomputes calendars when the native listener fires', async () => {
|
|
71
|
+
mount(ROOT_TAG, createElement(Probe));
|
|
72
|
+
|
|
73
|
+
const invalidate = addListener.mock.calls[0][0];
|
|
74
|
+
getCalendars.mockReturnValue(FAKE_CALENDARS_UPDATED);
|
|
75
|
+
invalidate();
|
|
76
|
+
|
|
77
|
+
await vi.waitFor(() => expect(results[results.length - 1]).toEqual(FAKE_CALENDARS_UPDATED));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('unsubscribes from the native listener on unmount', () => {
|
|
81
|
+
mount(ROOT_TAG, createElement(Probe));
|
|
82
|
+
|
|
83
|
+
unmount(ROOT_TAG);
|
|
84
|
+
|
|
85
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// React lifecycle wiring over the framework-agnostic core (core/localization.ts /
|
|
2
|
+
// core/native-module.ts). Ported from expo-localization's own useLocales hook (sdk-57): a
|
|
3
|
+
// useReducer-driven invalidation counter that useMemo recomputes getLocales() from, whenever the
|
|
4
|
+
// native module fires onLocaleSettingsChanged.
|
|
5
|
+
import { useEffect, useMemo, useReducer } from 'react';
|
|
6
|
+
import { addLocaleListener, getLocales, type Locale } from '../../../core';
|
|
7
|
+
|
|
8
|
+
export function useLocales(): Locale[] {
|
|
9
|
+
const [invalidationKey, invalidate] = useReducer((key: number) => key + 1, 0);
|
|
10
|
+
const locales = useMemo(() => getLocales(), [invalidationKey]);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const subscription = addLocaleListener(invalidate);
|
|
14
|
+
return () => subscription.remove();
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
return locales;
|
|
18
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Co-located React-driven test (ADR 0025) for useLocales. Mocks `core`, not expo-modules-core
|
|
2
|
+
// internals — same pattern packages/battery's hook tests use.
|
|
3
|
+
|
|
4
|
+
import { createElement, type ReactElement } from 'react';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { mount, unmount, View } from '@symbiote-native/react';
|
|
7
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
8
|
+
import type { Locale } from '../../../core';
|
|
9
|
+
import { useLocales } from './index';
|
|
10
|
+
|
|
11
|
+
const FAKE_LOCALES_INITIAL: Locale[] = [
|
|
12
|
+
{
|
|
13
|
+
languageTag: 'pl-PL',
|
|
14
|
+
languageCode: 'pl',
|
|
15
|
+
languageScriptCode: 'Latn',
|
|
16
|
+
regionCode: 'PL',
|
|
17
|
+
languageRegionCode: 'PL',
|
|
18
|
+
currencyCode: 'PLN',
|
|
19
|
+
currencySymbol: 'zł',
|
|
20
|
+
languageCurrencyCode: 'PLN',
|
|
21
|
+
languageCurrencySymbol: 'zł',
|
|
22
|
+
decimalSeparator: ',',
|
|
23
|
+
digitGroupingSeparator: ' ',
|
|
24
|
+
textDirection: 'ltr',
|
|
25
|
+
measurementSystem: 'metric',
|
|
26
|
+
temperatureUnit: 'celsius',
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const FAKE_LOCALES_UPDATED: Locale[] = [
|
|
31
|
+
{
|
|
32
|
+
languageTag: 'en-US',
|
|
33
|
+
languageCode: 'en',
|
|
34
|
+
languageScriptCode: 'Latn',
|
|
35
|
+
regionCode: 'US',
|
|
36
|
+
languageRegionCode: 'US',
|
|
37
|
+
currencyCode: 'USD',
|
|
38
|
+
currencySymbol: '$',
|
|
39
|
+
languageCurrencyCode: 'USD',
|
|
40
|
+
languageCurrencySymbol: '$',
|
|
41
|
+
decimalSeparator: '.',
|
|
42
|
+
digitGroupingSeparator: ',',
|
|
43
|
+
textDirection: 'ltr',
|
|
44
|
+
measurementSystem: 'us',
|
|
45
|
+
temperatureUnit: 'fahrenheit',
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const { addListener, getLocales, remove } = vi.hoisted(() => {
|
|
50
|
+
const remove = vi.fn();
|
|
51
|
+
return {
|
|
52
|
+
addListener: vi.fn((_listener: () => void) => ({ remove })),
|
|
53
|
+
getLocales: vi.fn(),
|
|
54
|
+
remove,
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
vi.mock('../../../core', () => ({
|
|
59
|
+
addLocaleListener: addListener,
|
|
60
|
+
getLocales,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
const ROOT_TAG = 954;
|
|
64
|
+
|
|
65
|
+
const results: Locale[][] = [];
|
|
66
|
+
|
|
67
|
+
function Probe(): ReactElement {
|
|
68
|
+
results.push(useLocales());
|
|
69
|
+
return createElement(View);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const fabric = installFabric();
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
fabric.reset();
|
|
76
|
+
results.length = 0;
|
|
77
|
+
vi.clearAllMocks();
|
|
78
|
+
getLocales.mockReturnValue(FAKE_LOCALES_INITIAL);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(() => unmount(ROOT_TAG));
|
|
82
|
+
|
|
83
|
+
describe('useLocales', () => {
|
|
84
|
+
it('reads the current locales synchronously on first render', () => {
|
|
85
|
+
mount(ROOT_TAG, createElement(Probe));
|
|
86
|
+
|
|
87
|
+
expect(results[results.length - 1]).toEqual(FAKE_LOCALES_INITIAL);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('recomputes locales when the native listener fires', async () => {
|
|
91
|
+
mount(ROOT_TAG, createElement(Probe));
|
|
92
|
+
|
|
93
|
+
const invalidate = addListener.mock.calls[0][0];
|
|
94
|
+
getLocales.mockReturnValue(FAKE_LOCALES_UPDATED);
|
|
95
|
+
invalidate();
|
|
96
|
+
|
|
97
|
+
await vi.waitFor(() => expect(results[results.length - 1]).toEqual(FAKE_LOCALES_UPDATED));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('unsubscribes from the native listener on unmount', () => {
|
|
101
|
+
mount(ROOT_TAG, createElement(Probe));
|
|
102
|
+
|
|
103
|
+
unmount(ROOT_TAG);
|
|
104
|
+
|
|
105
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @symbiote-native/localization/react: the React entry over the framework-agnostic core. Locales
|
|
2
|
+
// and calendars each get their own lifecycle hook, matching upstream's own useLocales/
|
|
3
|
+
// useCalendars being two separate hooks, not one combined hook — mirrors the lifecycle-bucket
|
|
4
|
+
// naming convention of adapters/react/src/hooks (never `composables`, that's Vue's term).
|
|
5
|
+
|
|
6
|
+
export { useLocales } from './hooks/use-locales';
|
|
7
|
+
export { useCalendars } from './hooks/use-calendars';
|
|
8
|
+
export {
|
|
9
|
+
getLocales,
|
|
10
|
+
getCalendars,
|
|
11
|
+
addLocaleListener,
|
|
12
|
+
addCalendarListener,
|
|
13
|
+
Weekday,
|
|
14
|
+
CalendarIdentifier,
|
|
15
|
+
type Locale,
|
|
16
|
+
type Calendar,
|
|
17
|
+
type EventSubscription,
|
|
18
|
+
} from '../core';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Vue lifecycle wiring over the framework-agnostic core — mirrors use-locales' shape exactly.
|
|
2
|
+
import { onMounted, onUnmounted, ref, type Ref } from '@vue/runtime-core';
|
|
3
|
+
import {
|
|
4
|
+
addCalendarListener,
|
|
5
|
+
getCalendars,
|
|
6
|
+
type Calendar,
|
|
7
|
+
type EventSubscription,
|
|
8
|
+
} from '../../../core';
|
|
9
|
+
|
|
10
|
+
export function useCalendars(): Ref<Calendar[]> {
|
|
11
|
+
const calendars = ref<Calendar[]>(getCalendars());
|
|
12
|
+
let subscription: EventSubscription | undefined;
|
|
13
|
+
|
|
14
|
+
onMounted(() => {
|
|
15
|
+
subscription = addCalendarListener(() => {
|
|
16
|
+
calendars.value = getCalendars();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
onUnmounted(() => {
|
|
21
|
+
subscription?.remove();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return calendars;
|
|
25
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Co-located Vue-driven test (ADR 0025) for useCalendars. See use-locales' test for the shared
|
|
2
|
+
// rationale.
|
|
3
|
+
|
|
4
|
+
import { defineComponent, h, type Ref } from '@vue/runtime-core';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { mount, unmount } from '@symbiote-native/vue';
|
|
7
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
8
|
+
import { CalendarIdentifier, Weekday, type Calendar } from '../../../core/types';
|
|
9
|
+
import { useCalendars } from './index';
|
|
10
|
+
|
|
11
|
+
const ROOT_TAG = 9955;
|
|
12
|
+
|
|
13
|
+
const FAKE_CALENDARS_INITIAL: Calendar[] = [
|
|
14
|
+
{
|
|
15
|
+
calendar: CalendarIdentifier.GREGORY,
|
|
16
|
+
uses24hourClock: true,
|
|
17
|
+
firstWeekday: Weekday.SUNDAY,
|
|
18
|
+
timeZone: 'Europe/Warsaw',
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const FAKE_CALENDARS_UPDATED: Calendar[] = [
|
|
23
|
+
{
|
|
24
|
+
calendar: CalendarIdentifier.ISO8601,
|
|
25
|
+
uses24hourClock: false,
|
|
26
|
+
firstWeekday: Weekday.MONDAY,
|
|
27
|
+
timeZone: 'America/New_York',
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
let registeredListener: (() => void) | undefined;
|
|
32
|
+
const removeMock = vi.fn();
|
|
33
|
+
const addListenerMock = vi.fn((listener: () => void) => {
|
|
34
|
+
registeredListener = listener;
|
|
35
|
+
return { remove: removeMock };
|
|
36
|
+
});
|
|
37
|
+
const getCalendarsMock = vi.fn(() => FAKE_CALENDARS_INITIAL);
|
|
38
|
+
|
|
39
|
+
vi.mock('../../../core', () => ({
|
|
40
|
+
addCalendarListener: (listener: () => void) => addListenerMock(listener),
|
|
41
|
+
getCalendars: () => getCalendarsMock(),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
const fabric = installFabric();
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
fabric.reset();
|
|
48
|
+
registeredListener = undefined;
|
|
49
|
+
addListenerMock.mockClear();
|
|
50
|
+
removeMock.mockClear();
|
|
51
|
+
getCalendarsMock.mockClear();
|
|
52
|
+
getCalendarsMock.mockReturnValue(FAKE_CALENDARS_INITIAL);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => unmount(ROOT_TAG));
|
|
56
|
+
|
|
57
|
+
function mountCalendars(): Ref<Calendar[]> {
|
|
58
|
+
let calendars: Ref<Calendar[]> | undefined;
|
|
59
|
+
mount(
|
|
60
|
+
ROOT_TAG,
|
|
61
|
+
defineComponent({
|
|
62
|
+
setup: () => {
|
|
63
|
+
calendars = useCalendars();
|
|
64
|
+
return () => h('symbiote-text', {}, 'calendars');
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
if (calendars === undefined) {
|
|
69
|
+
throw new Error('setup() did not run');
|
|
70
|
+
}
|
|
71
|
+
return calendars;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe('useCalendars (Vue)', () => {
|
|
75
|
+
it('reads the current calendars synchronously at setup', () => {
|
|
76
|
+
const calendars = mountCalendars();
|
|
77
|
+
|
|
78
|
+
expect(calendars.value).toEqual(FAKE_CALENDARS_INITIAL);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('recomputes the ref when the native listener fires', () => {
|
|
82
|
+
const calendars = mountCalendars();
|
|
83
|
+
|
|
84
|
+
getCalendarsMock.mockReturnValue(FAKE_CALENDARS_UPDATED);
|
|
85
|
+
registeredListener?.();
|
|
86
|
+
|
|
87
|
+
expect(calendars.value).toEqual(FAKE_CALENDARS_UPDATED);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('removes the subscription on unmount', () => {
|
|
91
|
+
mountCalendars();
|
|
92
|
+
unmount(ROOT_TAG);
|
|
93
|
+
|
|
94
|
+
expect(removeMock).toHaveBeenCalledTimes(1);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Vue lifecycle wiring over the framework-agnostic core (core/localization.ts). Unlike
|
|
2
|
+
// packages/network's use-network-state (which seeds a placeholder and fetches asynchronously in
|
|
3
|
+
// onMounted), getLocales() is a synchronous native call, so the ref is computed once, directly at
|
|
4
|
+
// setup — onMounted only wires the change listener, mirroring use-network-state's
|
|
5
|
+
// onMounted/onUnmounted subscription shape otherwise.
|
|
6
|
+
import { onMounted, onUnmounted, ref, type Ref } from '@vue/runtime-core';
|
|
7
|
+
import { addLocaleListener, getLocales, type EventSubscription, type Locale } from '../../../core';
|
|
8
|
+
|
|
9
|
+
export function useLocales(): Ref<Locale[]> {
|
|
10
|
+
const locales = ref<Locale[]>(getLocales());
|
|
11
|
+
let subscription: EventSubscription | undefined;
|
|
12
|
+
|
|
13
|
+
onMounted(() => {
|
|
14
|
+
subscription = addLocaleListener(() => {
|
|
15
|
+
locales.value = getLocales();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
onUnmounted(() => {
|
|
20
|
+
subscription?.remove();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return locales;
|
|
24
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Co-located Vue-driven test (ADR 0025) for useLocales. See packages/battery's
|
|
2
|
+
// use-battery-state.test.ts for the shared rationale.
|
|
3
|
+
|
|
4
|
+
import { defineComponent, h, type Ref } from '@vue/runtime-core';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { mount, unmount } from '@symbiote-native/vue';
|
|
7
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
8
|
+
import type { Locale } from '../../../core';
|
|
9
|
+
import { useLocales } from './index';
|
|
10
|
+
|
|
11
|
+
const ROOT_TAG = 9954;
|
|
12
|
+
|
|
13
|
+
const FAKE_LOCALES_INITIAL: Locale[] = [
|
|
14
|
+
{
|
|
15
|
+
languageTag: 'pl-PL',
|
|
16
|
+
languageCode: 'pl',
|
|
17
|
+
languageScriptCode: 'Latn',
|
|
18
|
+
regionCode: 'PL',
|
|
19
|
+
languageRegionCode: 'PL',
|
|
20
|
+
currencyCode: 'PLN',
|
|
21
|
+
currencySymbol: 'zł',
|
|
22
|
+
languageCurrencyCode: 'PLN',
|
|
23
|
+
languageCurrencySymbol: 'zł',
|
|
24
|
+
decimalSeparator: ',',
|
|
25
|
+
digitGroupingSeparator: ' ',
|
|
26
|
+
textDirection: 'ltr',
|
|
27
|
+
measurementSystem: 'metric',
|
|
28
|
+
temperatureUnit: 'celsius',
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const FAKE_LOCALES_UPDATED: Locale[] = [
|
|
33
|
+
{
|
|
34
|
+
languageTag: 'en-US',
|
|
35
|
+
languageCode: 'en',
|
|
36
|
+
languageScriptCode: 'Latn',
|
|
37
|
+
regionCode: 'US',
|
|
38
|
+
languageRegionCode: 'US',
|
|
39
|
+
currencyCode: 'USD',
|
|
40
|
+
currencySymbol: '$',
|
|
41
|
+
languageCurrencyCode: 'USD',
|
|
42
|
+
languageCurrencySymbol: '$',
|
|
43
|
+
decimalSeparator: '.',
|
|
44
|
+
digitGroupingSeparator: ',',
|
|
45
|
+
textDirection: 'ltr',
|
|
46
|
+
measurementSystem: 'us',
|
|
47
|
+
temperatureUnit: 'fahrenheit',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
let registeredListener: (() => void) | undefined;
|
|
52
|
+
const removeMock = vi.fn();
|
|
53
|
+
const addListenerMock = vi.fn((listener: () => void) => {
|
|
54
|
+
registeredListener = listener;
|
|
55
|
+
return { remove: removeMock };
|
|
56
|
+
});
|
|
57
|
+
const getLocalesMock = vi.fn(() => FAKE_LOCALES_INITIAL);
|
|
58
|
+
|
|
59
|
+
vi.mock('../../../core', () => ({
|
|
60
|
+
addLocaleListener: (listener: () => void) => addListenerMock(listener),
|
|
61
|
+
getLocales: () => getLocalesMock(),
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
const fabric = installFabric();
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
fabric.reset();
|
|
68
|
+
registeredListener = undefined;
|
|
69
|
+
addListenerMock.mockClear();
|
|
70
|
+
removeMock.mockClear();
|
|
71
|
+
getLocalesMock.mockClear();
|
|
72
|
+
getLocalesMock.mockReturnValue(FAKE_LOCALES_INITIAL);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(() => unmount(ROOT_TAG));
|
|
76
|
+
|
|
77
|
+
function mountLocales(): Ref<Locale[]> {
|
|
78
|
+
let locales: Ref<Locale[]> | undefined;
|
|
79
|
+
mount(
|
|
80
|
+
ROOT_TAG,
|
|
81
|
+
defineComponent({
|
|
82
|
+
setup: () => {
|
|
83
|
+
locales = useLocales();
|
|
84
|
+
return () => h('symbiote-text', {}, 'locales');
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
if (locales === undefined) {
|
|
89
|
+
throw new Error('setup() did not run');
|
|
90
|
+
}
|
|
91
|
+
return locales;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe('useLocales (Vue)', () => {
|
|
95
|
+
it('reads the current locales synchronously at setup', () => {
|
|
96
|
+
const locales = mountLocales();
|
|
97
|
+
|
|
98
|
+
expect(locales.value).toEqual(FAKE_LOCALES_INITIAL);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('recomputes the ref when the native listener fires', () => {
|
|
102
|
+
const locales = mountLocales();
|
|
103
|
+
|
|
104
|
+
getLocalesMock.mockReturnValue(FAKE_LOCALES_UPDATED);
|
|
105
|
+
registeredListener?.();
|
|
106
|
+
|
|
107
|
+
expect(locales.value).toEqual(FAKE_LOCALES_UPDATED);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('removes the subscription on unmount', () => {
|
|
111
|
+
mountLocales();
|
|
112
|
+
unmount(ROOT_TAG);
|
|
113
|
+
|
|
114
|
+
expect(removeMock).toHaveBeenCalledTimes(1);
|
|
115
|
+
});
|
|
116
|
+
});
|