diff --git a/src/CONST.js b/src/CONST.js index 1b0a49e36d18..ac5bda74851c 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -84,6 +84,7 @@ const CONST = { PAY_WITH_EXPENSIFY: 'payWithExpensify', FREE_PLAN: 'freePlan', DEFAULT_ROOMS: 'defaultRooms', + INTERNATIONALIZATION: 'internationalization', }, BUTTON_STATES: { DEFAULT: 'default', diff --git a/src/components/LocalePicker.js b/src/components/LocalePicker.js new file mode 100644 index 000000000000..88a25971d7a3 --- /dev/null +++ b/src/components/LocalePicker.js @@ -0,0 +1,88 @@ +import React from 'react'; +import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; +import styles from '../styles/styles'; +import Picker from './Picker'; +import Text from './Text'; +import compose from '../libs/compose'; +import {setLocale} from '../libs/actions/App'; +import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import ONYXKEYS from '../ONYXKEYS'; +import CONST from '../CONST'; +import Permissions from '../libs/Permissions'; +import {translate} from '../libs/translate'; + +const propTypes = { + /** Indicates which locale the user currently has selected */ + preferredLocale: PropTypes.string, + + /** Indicates size of a picker component and whether to render the label or not */ + size: PropTypes.oneOf(['normal', 'small']), + + /** Beta features list */ + betas: PropTypes.arrayOf(PropTypes.string), + + ...withLocalizePropTypes, +}; + +const defaultProps = { + preferredLocale: CONST.DEFAULT_LOCALE, + size: 'normal', + betas: [], +}; + +const localesToLanguages = { + default: { + value: 'en', + label: translate('en', 'preferencesPage.languages.english'), + }, + es: { + value: 'es', + label: translate('es', 'preferencesPage.languages.spanish'), + }, +}; + +const LocalePicker = ({ + // eslint-disable-next-line no-shadow + preferredLocale, translate, betas, size, +}) => { + if (!Permissions.canUseInternationalization(betas)) { + return null; + } + + return ( + <> + {size === 'normal' && ( + + {translate('preferencesPage.language')} + + )} + { + if (locale !== preferredLocale) { + setLocale(locale); + } + }} + items={Object.values(localesToLanguages)} + size={size} + value={preferredLocale} + /> + + ); +}; + +LocalePicker.defaultProps = defaultProps; +LocalePicker.propTypes = propTypes; +LocalePicker.displayName = 'LocalePicker'; + +export default compose( + withLocalize, + withOnyx({ + preferredLocale: { + key: ONYXKEYS.PREFERRED_LOCALE, + }, + betas: { + key: ONYXKEYS.BETAS, + }, + }), +)(LocalePicker); diff --git a/src/components/Picker/PickerPropTypes.js b/src/components/Picker/PickerPropTypes.js index 633966a2bef4..3a6077b5886c 100644 --- a/src/components/Picker/PickerPropTypes.js +++ b/src/components/Picker/PickerPropTypes.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import Icon from '../Icon'; +import styles from '../../styles/styles'; import {DownArrow} from '../Icon/Expensicons'; const propTypes = { @@ -36,13 +37,32 @@ const propTypes = { /** An icon to display with the picker */ icon: PropTypes.func, + + /** Size of a picker component */ + size: PropTypes.oneOf(['normal', 'small']), }; const defaultProps = { useDisabledStyles: false, disabled: false, placeholder: {}, value: null, - icon: () => , + icon: size => ( + <> + {size === 'small' + ? ( + + ) + : ( + + )} + + ), }; export { diff --git a/src/components/Picker/index.js b/src/components/Picker/index.js index 427d38184ef3..b963c81601b8 100644 --- a/src/components/Picker/index.js +++ b/src/components/Picker/index.js @@ -13,19 +13,29 @@ const Picker = ({ value, icon, disabled, -}) => ( - -); + size, +}) => { + let pickerStyles; + if (size === 'small') { + pickerStyles = styles.pickerSmall; + } else { + pickerStyles = useDisabledStyles ? pickerDisabledStyles : styles.picker; + } + + return ( + icon(size)} + disabled={disabled} + fixAndroidTouchableBug + /> + ); +}; Picker.propTypes = pickerPropTypes.propTypes; Picker.defaultProps = pickerPropTypes.defaultProps; diff --git a/src/libs/Permissions.js b/src/libs/Permissions.js index 7063273b245c..1ca31ce88712 100644 --- a/src/libs/Permissions.js +++ b/src/libs/Permissions.js @@ -51,10 +51,19 @@ function canUseDefaultRooms(betas) { return _.contains(betas, CONST.BETAS.DEFAULT_ROOMS) || canUseAllBetas(betas); } +/** + * @param {Array} betas + * @returns {Boolean} + */ +function canUseInternationalization(betas) { + return _.contains(betas, CONST.BETAS.INTERNATIONALIZATION) || canUseAllBetas(betas); +} + export default { canUseChronos, canUseIOU, canUsePayWithExpensify, canUseFreePlan, canUseDefaultRooms, + canUseInternationalization, }; diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index 583753ec8d3e..a206b5f249ac 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -19,6 +19,12 @@ Onyx.connect({ }, }); +let currentPreferredLocale; +Onyx.connect({ + key: ONYXKEYS.PREFERRED_LOCALE, + callback: val => currentPreferredLocale = val, +}); + /** * Clears the Onyx store and redirects to the sign in page. * Normally this method would live in Session.js, but that would cause a circular dependency with Network.js. @@ -37,12 +43,16 @@ function redirectToSignIn(errorMessage) { } const activeClients = currentActiveClients; + const preferredLocale = currentPreferredLocale; // We must set the authToken to null so we can navigate to "signin" it's not possible to navigate to the route as // it only exists when the authToken is null. Onyx.set(ONYXKEYS.SESSION, {authToken: null}) .then(() => { Onyx.clear().then(() => { + if (preferredLocale) { + Onyx.set(ONYXKEYS.PREFERRED_LOCALE, preferredLocale); + } if (errorMessage) { Onyx.set(ONYXKEYS.SESSION, {error: errorMessage}); } diff --git a/src/pages/settings/PreferencesPage.js b/src/pages/settings/PreferencesPage.js index 009150001840..03e8c23c017c 100755 --- a/src/pages/settings/PreferencesPage.js +++ b/src/pages/settings/PreferencesPage.js @@ -4,6 +4,7 @@ import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; +import LocalePicker from '../../components/LocalePicker'; import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; import ONYXKEYS from '../../ONYXKEYS'; @@ -17,7 +18,6 @@ import Switch from '../../components/Switch'; import Picker from '../../components/Picker'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import compose from '../../libs/compose'; -import {setLocale} from '../../libs/actions/App'; const propTypes = { /** The chat priority mode */ @@ -29,20 +29,16 @@ const propTypes = { expensifyNewsStatus: PropTypes.bool, }), - /** Indicates which locale the user currently has selected */ - preferredLocale: PropTypes.string, - ...withLocalizePropTypes, }; const defaultProps = { priorityMode: CONST.PRIORITY_MODE.DEFAULT, user: {}, - preferredLocale: CONST.DEFAULT_LOCALE, }; const PreferencesPage = ({ - priorityMode, user, translate, preferredLocale, + priorityMode, user, translate, }) => { const priorityModes = { default: { @@ -57,17 +53,6 @@ const PreferencesPage = ({ }, }; - const localesToLanguages = { - default: { - value: 'en', - label: translate('preferencesPage.languages.english'), - }, - es: { - value: 'es', - label: translate('preferencesPage.languages.spanish'), - }, - }; - return ( {priorityModes[priorityMode].description} - - {translate('preferencesPage.language')} - - { - if (locale !== preferredLocale) { - setLocale(locale); - } - }} - items={Object.values(localesToLanguages)} - value={preferredLocale} - /> + @@ -142,8 +116,5 @@ export default compose( user: { key: ONYXKEYS.USER, }, - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, }), )(PreferencesPage); diff --git a/src/pages/signin/TermsAndLicenses/TermsWithLicenses/index.android.js b/src/pages/signin/TermsAndLicenses/TermsWithLicenses/index.android.js index fa0d4cbed3c6..4ef064130e71 100644 --- a/src/pages/signin/TermsAndLicenses/TermsWithLicenses/index.android.js +++ b/src/pages/signin/TermsAndLicenses/TermsWithLicenses/index.android.js @@ -7,12 +7,10 @@ import withLocalize, { withLocalizePropTypes, } from '../../../../components/withLocalize'; import LogoWordmark from '../../../../../assets/images/expensify-wordmark.svg'; +import LocalePicker from '../../../../components/LocalePicker'; const TermsWithLicenses = ({translate}) => ( - - - ( styles.flexWrap, styles.textAlignCenter, styles.alignItemsCenter, - styles.justifyContentCenter, ]} > @@ -61,6 +58,12 @@ const TermsWithLicenses = ({translate}) => ( . + + + + ); diff --git a/src/pages/signin/TermsAndLicenses/TermsWithLicenses/index.ios.js b/src/pages/signin/TermsAndLicenses/TermsWithLicenses/index.ios.js index 720c89434bf3..f884c94d7d8e 100644 --- a/src/pages/signin/TermsAndLicenses/TermsWithLicenses/index.ios.js +++ b/src/pages/signin/TermsAndLicenses/TermsWithLicenses/index.ios.js @@ -7,19 +7,16 @@ import withLocalize, { withLocalizePropTypes, } from '../../../../components/withLocalize'; import LogoWordmark from '../../../../../assets/images/expensify-wordmark.svg'; +import LocalePicker from '../../../../components/LocalePicker'; const TermsWithLicenses = ({translate}) => ( - - - @@ -67,6 +64,12 @@ const TermsWithLicenses = ({translate}) => ( . + + + + ); diff --git a/src/pages/signin/TermsAndLicenses/TermsWithLicenses/index.js b/src/pages/signin/TermsAndLicenses/TermsWithLicenses/index.js index 175bda985afc..a022e771f31b 100755 --- a/src/pages/signin/TermsAndLicenses/TermsWithLicenses/index.js +++ b/src/pages/signin/TermsAndLicenses/TermsWithLicenses/index.js @@ -7,13 +7,11 @@ import withLocalize, { withLocalizePropTypes, } from '../../../../components/withLocalize'; import LogoWordmark from '../../../../../assets/images/expensify-wordmark.svg'; +import LocalePicker from '../../../../components/LocalePicker'; const TermsWithLicenses = ({translate}) => ( - - - - + {translate('termsOfUse.phrase1')} {' '} ( . + + + + ); diff --git a/src/styles/styles.js b/src/styles/styles.js index a5c5f0d3e90f..7d4572d835cb 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -340,6 +340,64 @@ const styles = { }, }, + pickerSmall: { + inputIOS: { + fontFamily: fontFamily.GTA, + fontSize: variables.fontSizeSmall, + paddingLeft: 9, + paddingRight: 25, + paddingTop: 6, + paddingBottom: 6, + borderRadius: variables.componentBorderRadius, + borderWidth: 1, + borderColor: themeColors.border, + color: themeColors.text, + height: variables.componentSizeSmall, + opacity: 1, + backgroundColor: 'transparent', + }, + inputWeb: { + fontFamily: fontFamily.GTA, + fontSize: variables.fontSizeSmall, + paddingLeft: 9, + paddingRight: 25, + paddingTop: 6, + paddingBottom: 6, + borderWidth: 1, + borderRadius: variables.componentBorderRadius, + borderColor: themeColors.border, + color: themeColors.text, + appearance: 'none', + height: variables.componentSizeSmall, + opacity: 1, + cursor: 'pointer', + backgroundColor: 'transparent', + }, + inputAndroid: { + fontFamily: fontFamily.GTA, + fontSize: variables.fontSizeSmall, + paddingLeft: 9, + paddingRight: 25, + paddingTop: 6, + paddingBottom: 6, + borderWidth: 1, + borderRadius: variables.componentBorderRadius, + borderColor: themeColors.border, + color: themeColors.text, + height: variables.componentSizeSmall, + opacity: 1, + }, + iconContainer: { + top: 7, + right: 9, + pointerEvents: 'none', + }, + icon: { + width: variables.iconSizeExtraSmall, + height: variables.iconSizeExtraSmall, + }, + }, + badge: { backgroundColor: themeColors.badgeDefaultBG, borderRadius: 14,