@amazon-devices/expo-localization 2.0.1758683737

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 (54) hide show
  1. package/.eslintrc.js +2 -0
  2. package/CHANGELOG.md +198 -0
  3. package/CMakeLists.txt +50 -0
  4. package/LICENSE +7 -0
  5. package/README.kepler.md +109 -0
  6. package/README.md +41 -0
  7. package/THIRD_PARTY_NOTICES.txt +24 -0
  8. package/android/build.gradle +109 -0
  9. package/android/src/main/AndroidManifest.xml +3 -0
  10. package/android/src/main/java/expo/modules/localization/LocalizationModule.kt +207 -0
  11. package/android/src/main/java/expo/modules/localization/LocalizationPackage.kt +35 -0
  12. package/android/src/main/java/expo/modules/localization/LocalizationUtils.kt +49 -0
  13. package/android/src/main/res/values/strings.xml +4 -0
  14. package/app.plugin.js +1 -0
  15. package/build/ExpoLocalization.d.ts +22 -0
  16. package/build/ExpoLocalization.d.ts.map +1 -0
  17. package/build/ExpoLocalization.js +166 -0
  18. package/build/ExpoLocalization.js.map +1 -0
  19. package/build/ExpoLocalization.kepler.d.ts +12 -0
  20. package/build/ExpoLocalization.kepler.d.ts.map +1 -0
  21. package/build/ExpoLocalization.kepler.js +26 -0
  22. package/build/ExpoLocalization.kepler.js.map +1 -0
  23. package/build/ExpoLocalization.native.d.ts +7 -0
  24. package/build/ExpoLocalization.native.d.ts.map +1 -0
  25. package/build/ExpoLocalization.native.js +14 -0
  26. package/build/ExpoLocalization.native.js.map +1 -0
  27. package/build/Localization.d.ts +163 -0
  28. package/build/Localization.d.ts.map +1 -0
  29. package/build/Localization.js +197 -0
  30. package/build/Localization.js.map +1 -0
  31. package/build/Localization.types.d.ts +202 -0
  32. package/build/Localization.types.d.ts.map +1 -0
  33. package/build/Localization.types.js +59 -0
  34. package/build/Localization.types.js.map +1 -0
  35. package/expo-module.config.json +10 -0
  36. package/ios/ExpoLocalization.podspec +32 -0
  37. package/ios/LocalizationModule.swift +212 -0
  38. package/kepler/ExpoLocalization.cpp +128 -0
  39. package/kepler/ExpoLocalization.h +22 -0
  40. package/kepler/KeplerInit.cpp +26 -0
  41. package/kepler/test/ExpoLocalization.cpp +78 -0
  42. package/package.json +1 -0
  43. package/plugin/build/withExpoLocalization.d.ts +7 -0
  44. package/plugin/build/withExpoLocalization.js +46 -0
  45. package/plugin/src/withExpoLocalization.ts +62 -0
  46. package/plugin/tsconfig.json +9 -0
  47. package/plugin/tsconfig.tsbuildinfo +1 -0
  48. package/react-native.config.js +21 -0
  49. package/src/ExpoLocalization.kepler.ts +40 -0
  50. package/src/ExpoLocalization.native.ts +18 -0
  51. package/src/ExpoLocalization.ts +203 -0
  52. package/src/Localization.ts +217 -0
  53. package/src/Localization.types.ts +207 -0
  54. package/tsconfig.json +9 -0
@@ -0,0 +1,21 @@
1
+ module.exports = {
2
+ dependencies: {
3
+ '@amazon-devices/react-native-kepler': {
4
+ platforms: {
5
+ kepler: null, // do not use platform definition from @react-native-kepler
6
+ },
7
+ },
8
+ },
9
+ dependency: {
10
+ platforms: {
11
+ kepler: {
12
+ autolink: {
13
+ ExpoLocalizationLib: {
14
+ provider: 'system',
15
+ turbomodules: ['ExpoLocalization'],
16
+ },
17
+ },
18
+ },
19
+ },
20
+ },
21
+ };
@@ -0,0 +1,40 @@
1
+ /*
2
+ * Copyright (c) 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ *
4
+ * PROPRIETARY/CONFIDENTIAL
5
+ *
6
+ * Use is subject to license terms.
7
+ */
8
+
9
+ import {KeplerTurboModule, TurboModuleRegistry} from '@amazon-devices/keplerscript-turbomodule-api';
10
+ import {Subscription} from 'expo-modules-core';
11
+
12
+ import {Calendar, Locale} from './Localization.types';
13
+
14
+ interface Spec extends KeplerTurboModule {
15
+ getLocales(): Locale[];
16
+ getCalendars(): Calendar[];
17
+ }
18
+
19
+ const nativeModule = TurboModuleRegistry.getEnforcing<Spec>('ExpoLocalization') as Spec;
20
+ const i18nManager = TurboModuleRegistry.getEnforcing('I18nManager') as any;
21
+
22
+ export function addLocaleListener(listener: (event) => void) {
23
+ i18nManager.addListener('Locale', listener);
24
+ }
25
+
26
+ export function addCalendarListener(listener: (event) => void) {
27
+ i18nManager.addListener('Timezone', listener);
28
+ }
29
+
30
+ export function removeSubscription(subscription: Subscription) {
31
+ i18nManager.removeListeners();
32
+ }
33
+
34
+ const module = {
35
+ getLocales: nativeModule.getLocales,
36
+ getCalendars: nativeModule.getCalendars,
37
+ locale: i18nManager.getAppLocale(),
38
+ };
39
+
40
+ export default module;
@@ -0,0 +1,18 @@
1
+ import { EventEmitter, Subscription, requireNativeModule } from 'expo-modules-core';
2
+
3
+ const ExpoLocalizationModule = requireNativeModule('ExpoLocalization');
4
+ const emitter = new EventEmitter(ExpoLocalizationModule);
5
+
6
+ export function addLocaleListener(listener: (event) => void): Subscription {
7
+ return emitter.addListener('onLocaleSettingsChanged', listener);
8
+ }
9
+
10
+ export function addCalendarListener(listener: (event) => void): Subscription {
11
+ return emitter.addListener('onCalendarSettingsChanged', listener);
12
+ }
13
+
14
+ export function removeSubscription(subscription: Subscription) {
15
+ return emitter.removeSubscription(subscription);
16
+ }
17
+
18
+ export default ExpoLocalizationModule;
@@ -0,0 +1,203 @@
1
+ /* eslint-env browser */
2
+ import { Platform, Subscription } from 'expo-modules-core';
3
+ import * as rtlDetect from 'rtl-detect';
4
+
5
+ import { Localization, Calendar, Locale, CalendarIdentifier } from './Localization.types';
6
+
7
+ const getNavigatorLocales = () => {
8
+ return Platform.isDOMAvailable ? navigator.languages || [navigator.language] : [];
9
+ };
10
+
11
+ type ExtendedLocale = Intl.Locale &
12
+ // typescript definitions for navigator language don't include some modern Intl properties
13
+ Partial<{
14
+ textInfo: { direction: 'ltr' | 'rtl' };
15
+ timeZones: string[];
16
+ weekInfo: { firstDay: number };
17
+ hourCycles: string[];
18
+ timeZone: string;
19
+ calendars: string[];
20
+ }>;
21
+
22
+ const WEB_LANGUAGE_CHANGE_EVENT = 'languagechange';
23
+ // https://wisevoter.com/country-rankings/countries-that-use-fahrenheit/
24
+ const USES_FAHRENHEIT = [
25
+ 'AG',
26
+ 'BZ',
27
+ 'VG',
28
+ 'FM',
29
+ 'MH',
30
+ 'MS',
31
+ 'KN',
32
+ 'BS',
33
+ 'CY',
34
+ 'TC',
35
+ 'US',
36
+ 'LR',
37
+ 'PW',
38
+ 'KY',
39
+ ];
40
+
41
+ export function addLocaleListener(listener: (event) => void): Subscription {
42
+ addEventListener(WEB_LANGUAGE_CHANGE_EVENT, listener);
43
+ return {
44
+ remove: () => removeEventListener(WEB_LANGUAGE_CHANGE_EVENT, listener),
45
+ };
46
+ }
47
+
48
+ export function addCalendarListener(listener: (event) => void): Subscription {
49
+ addEventListener(WEB_LANGUAGE_CHANGE_EVENT, listener);
50
+ return {
51
+ remove: () => removeEventListener(WEB_LANGUAGE_CHANGE_EVENT, listener),
52
+ };
53
+ }
54
+
55
+ export function removeSubscription(subscription: Subscription) {
56
+ subscription.remove();
57
+ }
58
+
59
+ export default {
60
+ get currency(): string | null {
61
+ // TODO: Add support
62
+ return null;
63
+ },
64
+ get decimalSeparator(): string {
65
+ return (1.1).toLocaleString().substring(1, 2);
66
+ },
67
+ get digitGroupingSeparator(): string {
68
+ const value = (1000).toLocaleString();
69
+ return value.length === 5 ? value.substring(1, 2) : '';
70
+ },
71
+ get isRTL(): boolean {
72
+ return rtlDetect.isRtlLang(this.locale) ?? false;
73
+ },
74
+ get isMetric(): boolean {
75
+ const { region } = this;
76
+ switch (region) {
77
+ case 'US': // USA
78
+ case 'LR': // Liberia
79
+ case 'MM': // Myanmar
80
+ return false;
81
+ }
82
+ return true;
83
+ },
84
+ get locale(): string {
85
+ if (!Platform.isDOMAvailable) {
86
+ return '';
87
+ }
88
+ const locale =
89
+ navigator.language ||
90
+ navigator['systemLanguage'] ||
91
+ navigator['browserLanguage'] ||
92
+ navigator['userLanguage'] ||
93
+ this.locales[0];
94
+ return locale;
95
+ },
96
+ get locales(): string[] {
97
+ if (!Platform.isDOMAvailable) {
98
+ return [];
99
+ }
100
+ const { languages = [] } = navigator;
101
+ return Array.from(languages);
102
+ },
103
+ get timezone(): string {
104
+ const defaultTimeZone = 'Etc/UTC';
105
+ if (typeof Intl === 'undefined') {
106
+ return defaultTimeZone;
107
+ }
108
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || defaultTimeZone;
109
+ },
110
+ get isoCurrencyCodes(): string[] {
111
+ // TODO(Bacon): Add this - very low priority
112
+ return [];
113
+ },
114
+ get region(): string | null {
115
+ // There is no way to obtain the current region, as is possible on native.
116
+ // Instead, use the country-code from the locale when possible (e.g. "en-US").
117
+ const { locale } = this;
118
+ const [, ...suffixes] = typeof locale === 'string' ? locale.split('-') : [];
119
+ for (const suffix of suffixes) {
120
+ if (suffix.length === 2) {
121
+ return suffix.toUpperCase();
122
+ }
123
+ }
124
+ return null;
125
+ },
126
+
127
+ getLocales(): Locale[] {
128
+ const locales = getNavigatorLocales();
129
+ return locales?.map((languageTag) => {
130
+ // TextInfo is an experimental API that is not available in all browsers.
131
+ // We might want to consider using a locale lookup table instead.
132
+ const locale =
133
+ typeof Intl !== 'undefined'
134
+ ? (new Intl.Locale(languageTag) as unknown as ExtendedLocale)
135
+ : { region: null, textInfo: null, language: null };
136
+ const { region, textInfo, language } = locale;
137
+
138
+ // Properties added only for compatibility with native, use `toLocaleString` instead.
139
+ const digitGroupingSeparator =
140
+ Array.from((10000).toLocaleString(languageTag)).filter((c) => c > '9' || c < '0')[0] ||
141
+ null; // using 1e5 instead of 1e4 since for some locales (like pl-PL) 1e4 does not use digit grouping
142
+ const decimalSeparator = (1.1).toLocaleString(languageTag).substring(1, 2);
143
+ const temperatureUnit = region ? regionToTemperatureUnit(region) : null;
144
+
145
+ return {
146
+ languageTag,
147
+ languageCode: language || languageTag.split('-')[0] || 'en',
148
+ textDirection: (textInfo?.direction as 'ltr' | 'rtl') || null,
149
+ digitGroupingSeparator,
150
+ decimalSeparator,
151
+ measurementSystem: null,
152
+ currencyCode: null,
153
+ currencySymbol: null,
154
+ regionCode: region || null,
155
+ temperatureUnit,
156
+ };
157
+ });
158
+ },
159
+ getCalendars(): Calendar[] {
160
+ const locale = ((typeof Intl !== 'undefined'
161
+ ? Intl.DateTimeFormat().resolvedOptions()
162
+ : null) ?? null) as unknown as null | ExtendedLocale;
163
+ return [
164
+ {
165
+ calendar: ((locale?.calendar || locale?.calendars?.[0]) as CalendarIdentifier) || null,
166
+ timeZone: locale?.timeZone || locale?.timeZones?.[0] || null,
167
+ uses24hourClock: (locale?.hourCycle || locale?.hourCycles?.[0])?.startsWith('h2') ?? null, //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle
168
+ firstWeekday: locale?.weekInfo?.firstDay || null,
169
+ },
170
+ ];
171
+ },
172
+
173
+ async getLocalizationAsync(): Promise<Omit<Localization, 'getCalendars' | 'getLocales'>> {
174
+ const {
175
+ currency,
176
+ decimalSeparator,
177
+ digitGroupingSeparator,
178
+ isoCurrencyCodes,
179
+ isMetric,
180
+ isRTL,
181
+ locale,
182
+ locales,
183
+ region,
184
+ timezone,
185
+ } = this;
186
+ return {
187
+ currency,
188
+ decimalSeparator,
189
+ digitGroupingSeparator,
190
+ isoCurrencyCodes,
191
+ isMetric,
192
+ isRTL,
193
+ locale,
194
+ locales,
195
+ region,
196
+ timezone,
197
+ };
198
+ },
199
+ };
200
+
201
+ function regionToTemperatureUnit(region: string) {
202
+ return USES_FAHRENHEIT.includes(region) ? 'fahrenheit' : 'celsius';
203
+ }
@@ -0,0 +1,217 @@
1
+ import { useEffect, useReducer, useMemo } from 'react';
2
+
3
+ import ExpoLocalization, {
4
+ addCalendarListener,
5
+ addLocaleListener,
6
+ removeSubscription,
7
+ } from './ExpoLocalization';
8
+ import { Localization } from './Localization.types';
9
+ export * from './Localization.types';
10
+
11
+ // @needsAudit
12
+ /**
13
+ * @deprecated Use Localization.getLocales() instead.
14
+ * Three-character ISO 4217 currency code. Returns `null` on web.
15
+ *
16
+ * @example `'USD'`, `'EUR'`, `'CNY'`, `null`
17
+ */
18
+ export const currency = ExpoLocalization.currency;
19
+
20
+ // @needsAudit
21
+ /**
22
+ * @deprecated Use Localization.getLocales() instead.
23
+ * Decimal separator used for formatting numbers.
24
+ *
25
+ * @example `','`, `'.'`
26
+ */
27
+ export const decimalSeparator = ExpoLocalization.decimalSeparator;
28
+
29
+ // @needsAudit
30
+ /**
31
+ * @deprecated Use Localization.getLocales() instead.
32
+ * Digit grouping separator used when formatting numbers larger than 1000.
33
+ *
34
+ * @example `'.'`, `''`, `','`
35
+ */
36
+ export const digitGroupingSeparator = ExpoLocalization.digitGroupingSeparator;
37
+
38
+ // @needsAudit
39
+ /**
40
+ * @deprecated Use Localization.getLocales() instead.
41
+ * A list of all the supported language ISO codes.
42
+ */
43
+ export const isoCurrencyCodes = ExpoLocalization.isoCurrencyCodes;
44
+
45
+ // @needsAudit
46
+ /**
47
+ * @deprecated Use Localization.getLocales() instead.
48
+ * Boolean value that indicates whether the system uses the metric system.
49
+ * On Android and web, this is inferred from the current region.
50
+ */
51
+ export const isMetric = ExpoLocalization.isMetric;
52
+
53
+ // @needsAudit
54
+ /**
55
+ * @deprecated Use Localization.getLocales() instead.
56
+ * Returns if the system's language is written from Right-to-Left.
57
+ * This can be used to build features like [bidirectional icons](https://material.io/design/usability/bidirectionality.html).
58
+ *
59
+ * Returns `false` in Server Side Rendering (SSR) environments.
60
+ */
61
+ export const isRTL = ExpoLocalization.isRTL;
62
+
63
+ // @needsAudit
64
+ /**
65
+ * Consider using Localization.getLocales() for a list of user preferred locales instead.
66
+ * An [IETF BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag),
67
+ * consisting of a two-character language code and optional script, region and variant codes.
68
+ *
69
+ * @example `'en'`, `'en-US'`, `'zh-Hans'`, `'zh-Hans-CN'`, `'en-emodeng'`
70
+ */
71
+ export const locale = ExpoLocalization.locale;
72
+
73
+ // @needsAudit
74
+ /**
75
+ * @deprecated Use Localization.getLocales() instead.
76
+ * List of all the native languages provided by the user settings.
77
+ * These are returned in the order the user defines in their device settings.
78
+ *
79
+ * @example `['en', 'en-US', 'zh-Hans', 'zh-Hans-CN', 'en-emodeng']`
80
+ */
81
+ export const locales = ExpoLocalization.locales;
82
+
83
+ // @needsAudit
84
+ /**
85
+ * @deprecated Use Localization.getCalendars() instead.
86
+ * The current time zone in display format.
87
+ * On Web time zone is calculated with Intl.DateTimeFormat().resolvedOptions().timeZone. For a
88
+ * better estimation you could use the moment-timezone package but it will add significant bloat to
89
+ * your website's bundle size.
90
+ *
91
+ * @example `'America/Los_Angeles'`
92
+ */
93
+ export const timezone = ExpoLocalization.timezone;
94
+
95
+ // @needsAudit
96
+ /**
97
+ * @deprecated Use Localization.getLocales() instead.
98
+ * The region code for your device that comes from the Region setting under Language & Region on iOS.
99
+ * This value is always available on iOS, but might return `null` on Android or web.
100
+ *
101
+ * @example `'US'`, `'NZ'`, `null`
102
+ */
103
+ export const region = ExpoLocalization.region;
104
+
105
+ /**
106
+ * List of user's locales, returned as an array of objects of type `Locale`.
107
+ * Guaranteed to contain at least 1 element.
108
+ * These are returned in the order the user defines in their device settings.
109
+ * On the web currency and measurements systems are not provided, instead returned as null.
110
+ * If needed, you can infer them from the current region using a lookup table.
111
+ * @example `[{
112
+ "languageTag": "pl-PL",
113
+ "languageCode": "pl",
114
+ "textDirection": "ltr",
115
+ "digitGroupingSeparator": " ",
116
+ "decimalSeparator": ",",
117
+ "measurementSystem": "metric",
118
+ "currencyCode": "PLN",
119
+ "currencySymbol": "zł",
120
+ "regionCode": "PL",
121
+ "temperatureUnit": "celsius"
122
+ }]`
123
+ */
124
+ export const getLocales = ExpoLocalization.getLocales;
125
+
126
+ /**
127
+ * List of user's preferred calendars, returned as an array of objects of type `Calendar`.
128
+ * Guaranteed to contain at least 1 element.
129
+ * For now always returns a single element, but it's likely to return a user preference list on some platforms in the future.
130
+ * @example `[
131
+ {
132
+ "calendar": "gregory",
133
+ "timeZone": "Europe/Warsaw",
134
+ "uses24hourClock": true,
135
+ "firstWeekday": 1
136
+ }
137
+ ]`
138
+ */
139
+ export const getCalendars = ExpoLocalization.getCalendars;
140
+
141
+ /**
142
+ * A hook providing a list of user's locales, returned as an array of objects of type `Locale`.
143
+ * Guaranteed to contain at least 1 element.
144
+ * These are returned in the order the user defines in their device settings.
145
+ * On the web currency and measurements systems are not provided, instead returned as null.
146
+ * If needed, you can infer them from the current region using a lookup table.
147
+ * If the OS settings change, the hook will rerender with a new list of locales.
148
+ * @example `[{
149
+ "languageTag": "pl-PL",
150
+ "languageCode": "pl",
151
+ "textDirection": "ltr",
152
+ "digitGroupingSeparator": " ",
153
+ "decimalSeparator": ",",
154
+ "measurementSystem": "metric",
155
+ "currencyCode": "PLN",
156
+ "currencySymbol": "zł",
157
+ "regionCode": "PL",
158
+ "temperatureUnit": "celsius"
159
+ }]`
160
+ */
161
+ export function useLocales() {
162
+ const [key, invalidate] = useReducer((k) => k + 1, 0);
163
+ const locales = useMemo(() => getLocales(), [key]);
164
+ useEffect(() => {
165
+ const subscription = addLocaleListener(invalidate);
166
+ return () => {
167
+ removeSubscription(subscription);
168
+ };
169
+ }, []);
170
+ return locales;
171
+ }
172
+
173
+ /**
174
+ * A hook providing a list of user's preferred calendars, returned as an array of objects of type `Calendar`.
175
+ * Guaranteed to contain at least 1 element.
176
+ * For now always returns a single element, but it's likely to return a user preference list on some platforms in the future.
177
+ * If the OS settings change, the hook will rerender with a new list of calendars.
178
+ * @example `[
179
+ {
180
+ "calendar": "gregory",
181
+ "timeZone": "Europe/Warsaw",
182
+ "uses24hourClock": true,
183
+ "firstWeekday": 1
184
+ }
185
+ ]`
186
+ */
187
+ export function useCalendars() {
188
+ const [key, invalidate] = useReducer((k) => k + 1, 0);
189
+ const calendars = useMemo(() => getCalendars(), [key]);
190
+ useEffect(() => {
191
+ const subscription = addCalendarListener(invalidate);
192
+ return () => {
193
+ removeSubscription(subscription);
194
+ };
195
+ }, []);
196
+ return calendars;
197
+ }
198
+
199
+ // @needsAudit
200
+ /**
201
+ * Get the latest native values from the device. Locale can be changed on some Android devices
202
+ * without resetting the app.
203
+ * > On iOS, changing the locale will cause the device to reset meaning the constants will always be
204
+ * correct.
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * // When the app returns from the background on Android...
209
+ *
210
+ * const { locale } = await Localization.getLocalizationAsync();
211
+ * ```
212
+ * @deprecated
213
+ * Use Localization.getLocales() or Localization.getCalendars() instead.
214
+ */
215
+ export async function getLocalizationAsync(): Promise<Localization> {
216
+ return await ExpoLocalization.getLocalizationAsync();
217
+ }