Skip to content

Commit

Permalink
[ENG-6235][localization] Add sync methods to expo localization (#19019)
Browse files Browse the repository at this point in the history
Co-authored-by: Łukasz Kosmaty <lukasz.kosmaty@swmansion.com>
Co-authored-by: Tomasz Sapeta <tomasz.sapeta@swmansion.com>
Co-authored-by: Aman Mittal <amandeepmittal@live.com>
Co-authored-by: Bartosz Kaszubowski <gosimek@gmail.com>
  • Loading branch information
5 people committed Oct 5, 2022
1 parent dee909a commit 1718521
Show file tree
Hide file tree
Showing 23 changed files with 872 additions and 44 deletions.
32 changes: 32 additions & 0 deletions apps/native-component-list/src/components/DeprecatedHeading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { PropsWithChildren } from 'react';
import { StyleSheet, Text, TextProps, View } from 'react-native';

import HeadingText from './HeadingText';

const DeprecatedHeading = ({ children, style }: PropsWithChildren<TextProps>) => (
<View style={styles.container}>
<Text style={styles.label}>Deprecated</Text>
<HeadingText style={styles.text}>{children}</HeadingText>
</View>
);

const styles = StyleSheet.create({
container: {
marginTop: 16,
},
label: {
color: '#db5739',
fontWeight: 'bold',
fontSize: 10,
},
text: {
fontWeight: 'bold',
fontSize: 18,
color: '#616161',
textDecorationLine: 'line-through',
textDecorationStyle: 'solid',
marginTop: -16,
},
});

export default DeprecatedHeading;
41 changes: 22 additions & 19 deletions apps/native-component-list/src/screens/LocalizationScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@ import chunk from 'lodash/chunk';
import React from 'react';
import { ScrollView, StyleSheet, Text, View } from 'react-native';

import DeprecatedHeading from '../components/DeprecatedHeading';
import HeadingText from '../components/HeadingText';
import ListButton from '../components/ListButton';
import MonoText from '../components/MonoText';

i18n.fallbacks = true;
i18n.locale = Localization.locale;
i18n.locale = Localization.getLocales()[0].languageTag;
i18n.translations = {
en: {
phrase: 'Hello my friend',
default: 'English language only',
},
ru: {
phrase: 'Привет мой друг',
es: {
phrase: 'Hola mi amigo',
},
};

Expand Down Expand Up @@ -81,37 +82,25 @@ export default class LocalizationScreen extends React.Component<{}, State> {
i18n.locale = locale;
this.setState({ locale });
};

render() {
return (
<ScrollView>
<View style={styles.container}>
<HeadingText>Current Locale</HeadingText>
<MonoText>{JSON.stringify(this.state.currentLocale, null, 2)}</MonoText>

<HeadingText>Locales in Preference Order</HeadingText>
<ListButton title="Show preferred Locales" onPress={this.queryPreferredLocales} />
{this.state.preferredLocales && this.state.preferredLocales.length > 0 && (
<MonoText>{JSON.stringify(this.state.preferredLocales, null, 2)}</MonoText>
)}
<MonoText>{JSON.stringify(Localization.getLocales(), null, 2)}</MonoText>

<HeadingText>Currency Codes</HeadingText>
<ListButton title="Show first 100 currency codes" onPress={this.queryCurrencyCodes} />
{this.state.isoCurrencyCodes && this.state.isoCurrencyCodes.length > 0 && (
<MonoText>{this.prettyFormatCurrency()}</MonoText>
)}
<HeadingText>Calendars in Preference Order</HeadingText>
<MonoText>{JSON.stringify(Localization.getCalendars(), null, 2)}</MonoText>

<HeadingText>Localization Table</HeadingText>
<Picker
style={styles.picker}
selectedValue={this.state.locale}
onValueChange={(value) => this.changeLocale(`${value}`)}>
<Picker.Item label="🇺🇸 English" value="en" />
<Picker.Item label="🇷🇺 Russian" value="ru" />
<Picker.Item label="🇪🇸 Spanish" value="es" />
</Picker>

<MonoText>{JSON.stringify(Localization, null, 2)}</MonoText>

<View style={styles.languageBox}>
<View style={styles.row}>
<Text>Exists in Both: </Text>
Expand All @@ -122,6 +111,20 @@ export default class LocalizationScreen extends React.Component<{}, State> {
<Text>{this.state.currentLocale ? i18n.t('default') : ''}</Text>
</View>
</View>

<DeprecatedHeading>Current Locale</DeprecatedHeading>
<MonoText>{JSON.stringify(this.state.currentLocale, null, 2)}</MonoText>
<DeprecatedHeading>Locales in Preference Order</DeprecatedHeading>
<ListButton title="Show preferred Locales" onPress={this.queryPreferredLocales} />
{this.state.preferredLocales && this.state.preferredLocales.length > 0 && (
<MonoText>{JSON.stringify(this.state.preferredLocales, null, 2)}</MonoText>
)}

<DeprecatedHeading>Currency Codes</DeprecatedHeading>
<ListButton title="Show first 100 currency codes" onPress={this.queryCurrencyCodes} />
{this.state.isoCurrencyCodes && this.state.isoCurrencyCodes.length > 0 && (
<MonoText>{this.prettyFormatCurrency()}</MonoText>
)}
</View>
</ScrollView>
);
Expand Down
49 changes: 49 additions & 0 deletions apps/test-suite/tests/Localization.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,55 @@ export function test(t) {
}

t.describe(`Localization methods`, () => {
t.it('expect to getLocales return preferred locales', () => {
const locales = Localization.getLocales();
t.expect(locales.length).toBeGreaterThanOrEqual(1);
const {
languageTag,
languageCode,
regionCode,
currencyCode,
currencySymbol,
decimalSeparator,
digitGroupingSeparator,
textDirection,
measurementSystem,
} = locales[0];
validateString(languageTag);
validateString(languageCode);
// following properties can be nullish if the locale does not provide/override them
t.expect(regionCode).toBeDefined();
t.expect(currencyCode).toBeDefined();
t.expect(currencySymbol).toBeDefined();
t.expect(decimalSeparator).toBeDefined();
t.expect(digitGroupingSeparator).toBeDefined();
t.expect(textDirection).toBeDefined();
if (textDirection) {
t.expect(['rtl', 'ltr'].includes(textDirection)).toBe(true);
}
t.expect(measurementSystem).toBeDefined();
if (measurementSystem) {
t.expect(['metric', 'us', 'uk'].includes(measurementSystem)).toBe(true);
}
});

t.it('expect getCalendars to return at least a single calendar', () => {
const calendars = Localization.getCalendars();
t.expect(calendars.length).toBeGreaterThanOrEqual(1);
const { calendar, uses24hourClock, firstWeekday, timeZone } = calendars[0];
t.expect(calendar).toBeDefined();
t.expect(timeZone).toBeDefined();
// following properties can be nullish if the locale does not provide/override them
t.expect(uses24hourClock).toBeDefined();
if (uses24hourClock !== null) {
t.expect(typeof uses24hourClock).toBe('boolean');
}
t.expect(firstWeekday).toBeDefined();
if (firstWeekday !== null) {
t.expect(typeof firstWeekday).toBe('number');
}
});

t.it('expect async to return locale', async () => {
const {
currency,
Expand Down
4 changes: 3 additions & 1 deletion docs/pages/versions/unversioned/sdk/localization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ import * as Localization from 'expo-localization';

### Behavior

This API is mostly synchronous and driven by constants. On iOS the constants will always be correct, on Android you should check if the locale has updated using `AppState` and `Localization.getLocalizationAsync()`. Initially the constants will be correct on both platforms, but on Android a user can change the language and return, more on this later.
You can use synchronous `getLocales()` and `getCalendars()` methods to get the locale settings of the user device. On iOS, the results will remain the same while the app is running.

On Android, the user can change locale preferences in Settings without restarting apps. To keep the localization current, you can rerun the `getLocales()` and `getCalendars()` methods every time the app returns to the foreground. Use `AppState` to detect this.

<APISection packageName="expo-localization" apiName="Localization" />
2 changes: 1 addition & 1 deletion docs/public/static/data/unversioned/expo-localization.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions packages/expo-localization/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@

### 🎉 New features

- Added two new synchronous functions: `getLocales` and `getCalendars`. ([#19019](https://github.com/expo/expo/pull/19019) by [@aleqsio](https://github.com/aleqsio))

### 🐛 Bug fixes

### ⚠️ Notices

- Deprecated existing constants API while keeping backwards compatibility. ([#19019](https://github.com/expo/expo/pull/19019) by [@aleqsio](https://github.com/aleqsio))

### 💡 Others

## 13.1.0 — 2022-07-07
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package expo.modules.localization

import android.icu.util.LocaleData
import android.icu.util.ULocale
import android.os.Bundle
import android.view.View
import android.text.TextUtils
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.text.TextUtils.getLayoutDirectionFromLocale
import android.text.format.DateFormat
import android.util.LayoutDirection
import androidx.core.os.LocaleListCompat
import androidx.core.os.bundleOf

import expo.modules.kotlin.modules.Module
Expand All @@ -25,6 +31,14 @@ class LocalizationModule : Module() {
AsyncFunction("getLocalizationAsync") {
return@AsyncFunction bundledConstants
}

Function("getLocales") {
return@Function getPreferredLocales()
}

Function("getCalendars") {
return@Function getCalendars()
}
}

// TODO: Bacon: add set language
Expand Down Expand Up @@ -73,6 +87,73 @@ class LocalizationModule : Module() {
getCountryCode(locale)
}
}

private fun getMeasurementSystem(locale: Locale): String? {
return if (VERSION.SDK_INT >= VERSION_CODES.P) {
when (LocaleData.getMeasurementSystem(ULocale.forLocale(locale))) {
LocaleData.MeasurementSystem.SI -> "metric"
LocaleData.MeasurementSystem.UK -> "uk"
LocaleData.MeasurementSystem.US -> "us"
else -> "metric"
}
} else {
if (getRegionCode(locale).equals("uk")) "uk"
else if (USES_IMPERIAL.contains(getRegionCode(locale))) "us"
else "metric"
}
}

private fun getPreferredLocales(): List<Map<String, Any?>> {
val locales = mutableListOf<Map<String, Any?>>()
val localeList: LocaleListCompat = LocaleListCompat.getDefault()
for (i in 0 until localeList.size()) {
val locale: Locale = localeList.get(i)
val decimalFormat = DecimalFormatSymbols.getInstance(locale)
locales.add(
mapOf(
"languageTag" to locale.toLanguageTag(),
"regionCode" to getRegionCode(locale),
"textDirection" to if (getLayoutDirectionFromLocale(locale) == LayoutDirection.RTL) "rtl" else "ltr",
"languageCode" to locale.language,

// the following two properties should be deprecated once Intl makes it way to RN, instead use toLocaleString
"decimalSeparator" to decimalFormat.decimalSeparator.toString(),
"digitGroupingSeparator" to decimalFormat.groupingSeparator.toString(),

"measurementSystem" to getMeasurementSystem(locale),
"currencyCode" to decimalFormat.currency.currencyCode,

// currency symbol can be localized to display locale (1st on the list) or to the locale for the currency (as done here).
"currencySymbol" to Currency.getInstance(locale).getSymbol(locale),
)
)
}
return locales
}

private fun uses24HourClock(): Boolean {
if (appContext.reactContext == null) return false
return DateFormat.is24HourFormat(appContext.reactContext)
}

private fun getCalendarType(): String {
return if (VERSION.SDK_INT >= VERSION_CODES.O) {
Calendar.getInstance().calendarType.toString()
} else {
"gregory"
}
}

private fun getCalendars(): List<Map<String, Any?>> {
return listOf(
mapOf(
"calendar" to getCalendarType(),
"uses24hourClock" to uses24HourClock(), // we ideally would use hourCycle (one of h12, h23, h11, h24) instead, but not sure how to get it on android and ios
"firstWeekday" to Calendar.getInstance().firstDayOfWeek,
"timeZone" to Calendar.getInstance().timeZone.id
)
)
}
}

/**
Expand Down
6 changes: 4 additions & 2 deletions packages/expo-localization/build/ExpoLocalization.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/expo-localization/build/ExpoLocalization.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions packages/expo-localization/build/ExpoLocalization.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 1718521

Please sign in to comment.