-
Notifications
You must be signed in to change notification settings - Fork 3.2k
[Fix] Localize currency symbol #8299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7890252
3fa9bc3
fad813c
0d56d96
1f50122
df003d6
3cd30fa
155dc02
3e57e26
6a0e400
84bcdeb
696b7d5
3ef60e8
5992dfc
b6d518c
21d3bc0
671f2d2
7c4aa7e
291782f
1841022
6147523
8e9764f
46148a3
f85bbde
04882ce
355e65d
fc91394
c25bf64
133a62e
f018a24
c93b4cb
d87f569
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import TextInput from './TextInput'; | ||
import styles from '../styles/styles'; | ||
import CONST from '../CONST'; | ||
|
||
const propTypes = { | ||
/** Formatted amount in local currency */ | ||
formattedAmount: PropTypes.string.isRequired, | ||
|
||
/** A ref to forward to amount text input */ | ||
forwardedRef: PropTypes.oneOfType([ | ||
PropTypes.func, | ||
PropTypes.shape({current: PropTypes.instanceOf(React.Component)}), | ||
]), | ||
|
||
/** Function to call when amount in text input is changed */ | ||
onChangeAmount: PropTypes.func.isRequired, | ||
|
||
/** Placeholder value for amount text input */ | ||
placeholder: PropTypes.string.isRequired, | ||
}; | ||
|
||
const defaultProps = { | ||
forwardedRef: undefined, | ||
}; | ||
|
||
function AmountTextInput(props) { | ||
return ( | ||
<TextInput | ||
disableKeyboard | ||
autoGrow | ||
hideFocusedState | ||
inputStyle={[styles.iouAmountTextInput, styles.p0, styles.noLeftBorderRadius, styles.noRightBorderRadius]} | ||
textInputContainerStyles={[styles.borderNone, styles.noLeftBorderRadius, styles.noRightBorderRadius]} | ||
onChangeText={props.onChangeAmount} | ||
ref={props.forwardedRef} | ||
value={props.formattedAmount} | ||
placeholder={props.placeholder} | ||
keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} | ||
/> | ||
); | ||
} | ||
|
||
AmountTextInput.propTypes = propTypes; | ||
AmountTextInput.defaultProps = defaultProps; | ||
AmountTextInput.displayName = 'AmountTextInput'; | ||
|
||
export default React.forwardRef((props, ref) => ( | ||
// eslint-disable-next-line react/jsx-props-no-spreading | ||
<AmountTextInput {...props} forwardedRef={ref} /> | ||
)); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import React from 'react'; | ||
import {TouchableOpacity} from 'react-native'; | ||
import PropTypes from 'prop-types'; | ||
import Text from './Text'; | ||
import styles from '../styles/styles'; | ||
|
||
const propTypes = { | ||
/** Currency symbol of selected currency */ | ||
currencySymbol: PropTypes.string.isRequired, | ||
|
||
/** Function to call when currency button is pressed */ | ||
onCurrencyButtonPress: PropTypes.func.isRequired, | ||
}; | ||
|
||
function CurrencySymbolButton(props) { | ||
return ( | ||
<TouchableOpacity onPress={props.onCurrencyButtonPress}> | ||
<Text style={styles.iouAmountText}>{props.currencySymbol}</Text> | ||
</TouchableOpacity> | ||
); | ||
} | ||
|
||
CurrencySymbolButton.propTypes = propTypes; | ||
CurrencySymbolButton.displayName = 'CurrencySymbolButton'; | ||
|
||
export default CurrencySymbolButton; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import AmountTextInput from './AmountTextInput'; | ||
import CurrencySymbolButton from './CurrencySymbolButton'; | ||
import * as CurrencySymbolUtils from '../libs/CurrencySymbolUtils'; | ||
|
||
const propTypes = { | ||
/** A ref to forward to amount text input */ | ||
forwardedRef: PropTypes.oneOfType([ | ||
PropTypes.func, | ||
PropTypes.shape({current: PropTypes.instanceOf(React.Component)}), | ||
]), | ||
|
||
/** Formatted amount in local currency */ | ||
formattedAmount: PropTypes.string.isRequired, | ||
|
||
/** Function to call when amount in text input is changed */ | ||
onChangeAmount: PropTypes.func, | ||
|
||
/** Function to call when currency button is pressed */ | ||
onCurrencyButtonPress: PropTypes.func, | ||
|
||
/** Placeholder value for amount text input */ | ||
placeholder: PropTypes.string.isRequired, | ||
|
||
/** Preferred locale of the user */ | ||
preferredLocale: PropTypes.string.isRequired, | ||
|
||
/** Currency code of user's selected currency */ | ||
selectedCurrencyCode: PropTypes.string.isRequired, | ||
}; | ||
|
||
const defaultProps = { | ||
forwardedRef: undefined, | ||
onChangeAmount: () => {}, | ||
onCurrencyButtonPress: () => {}, | ||
}; | ||
|
||
function TextInputWithCurrencySymbol(props) { | ||
const currencySymbol = CurrencySymbolUtils.getLocalizedCurrencySymbol(props.preferredLocale, props.selectedCurrencyCode); | ||
const isCurrencySymbolLTR = CurrencySymbolUtils.isCurrencySymbolLTR(props.preferredLocale, props.selectedCurrencyCode); | ||
|
||
const currencySymbolButton = ( | ||
<CurrencySymbolButton | ||
currencySymbol={currencySymbol} | ||
onCurrencyButtonPress={props.onCurrencyButtonPress} | ||
/> | ||
); | ||
|
||
const amountTextInput = ( | ||
<AmountTextInput | ||
formattedAmount={props.formattedAmount} | ||
onChangeAmount={props.onChangeAmount} | ||
placeholder={props.placeholder} | ||
ref={props.forwardedRef} | ||
/> | ||
); | ||
|
||
if (isCurrencySymbolLTR) { | ||
return ( | ||
<> | ||
{currencySymbolButton} | ||
{amountTextInput} | ||
</> | ||
); | ||
} | ||
|
||
return ( | ||
<> | ||
{amountTextInput} | ||
{currencySymbolButton} | ||
</> | ||
); | ||
} | ||
|
||
TextInputWithCurrencySymbol.propTypes = propTypes; | ||
TextInputWithCurrencySymbol.defaultProps = defaultProps; | ||
TextInputWithCurrencySymbol.displayName = 'TextInputWithCurrencySymbol'; | ||
|
||
export default React.forwardRef((props, ref) => ( | ||
// eslint-disable-next-line react/jsx-props-no-spreading | ||
<TextInputWithCurrencySymbol {...props} forwardedRef={ref} /> | ||
)); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import _ from 'underscore'; | ||
import * as NumberFormatUtils from './NumberFormatUtils'; | ||
|
||
/** | ||
* Get localized currency symbol for currency(ISO 4217) Code | ||
* @param {String} preferredLocale | ||
* @param {String} currencyCode | ||
* @returns {String} | ||
*/ | ||
function getLocalizedCurrencySymbol(preferredLocale, currencyCode) { | ||
const parts = NumberFormatUtils.formatToParts(preferredLocale, 0, { | ||
style: 'currency', | ||
currency: currencyCode, | ||
}); | ||
return _.find(parts, part => part.type === 'currency').value; | ||
} | ||
|
||
/** | ||
* Whether the currency symbol is left-to-right. | ||
* @param {String} preferredLocale | ||
* @param {String} currencyCode | ||
* @returns {Boolean} | ||
*/ | ||
function isCurrencySymbolLTR(preferredLocale, currencyCode) { | ||
const parts = NumberFormatUtils.formatToParts(preferredLocale, 0, { | ||
style: 'currency', | ||
currency: currencyCode, | ||
}); | ||
|
||
// Currency is LTR when the first part is of currency type. | ||
return parts[0].type === 'currency'; | ||
} | ||
|
||
export { | ||
getLocalizedCurrencySymbol, | ||
isCurrencySymbolLTR, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
import React from 'react'; | ||
import { | ||
View, | ||
TouchableOpacity, | ||
InteractionManager, | ||
} from 'react-native'; | ||
import PropTypes from 'prop-types'; | ||
|
@@ -16,10 +15,9 @@ import ROUTES from '../../../ROUTES'; | |
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; | ||
import compose from '../../../libs/compose'; | ||
import Button from '../../../components/Button'; | ||
import Text from '../../../components/Text'; | ||
import CONST from '../../../CONST'; | ||
import TextInput from '../../../components/TextInput'; | ||
import canUseTouchScreen from '../../../libs/canUseTouchscreen'; | ||
import TextInputWithCurrencySymbol from '../../../components/TextInputWithCurrencySymbol'; | ||
|
||
const propTypes = { | ||
/** Whether or not this IOU has multiple participants */ | ||
|
@@ -31,18 +29,6 @@ const propTypes = { | |
/** Callback to inform parent modal of success */ | ||
onStepComplete: PropTypes.func.isRequired, | ||
|
||
/** The currency list constant object from Onyx */ | ||
currencyList: PropTypes.objectOf(PropTypes.shape({ | ||
/** Symbol for the currency */ | ||
symbol: PropTypes.string, | ||
|
||
/** Name of the currency */ | ||
name: PropTypes.string, | ||
|
||
/** ISO4217 Code for the currency */ | ||
ISO4217: PropTypes.string, | ||
})).isRequired, | ||
|
||
/** Previously selected amount to show if the user comes back to this screen */ | ||
selectedAmount: PropTypes.string.isRequired, | ||
|
||
|
@@ -75,6 +61,7 @@ class IOUAmountPage extends React.Component { | |
this.updateAmount = this.updateAmount.bind(this); | ||
this.stripCommaFromAmount = this.stripCommaFromAmount.bind(this); | ||
this.focusTextInput = this.focusTextInput.bind(this); | ||
this.navigateToCurrencySelectionPage = this.navigateToCurrencySelectionPage.bind(this); | ||
|
||
this.state = { | ||
amount: props.selectedAmount, | ||
|
@@ -205,8 +192,19 @@ class IOUAmountPage extends React.Component { | |
.value(); | ||
} | ||
|
||
navigateToCurrencySelectionPage() { | ||
if (this.props.hasMultipleParticipants) { | ||
return Navigation.navigate(ROUTES.getIouBillCurrencyRoute(this.props.reportID)); | ||
} | ||
if (this.props.iouType === CONST.IOU.IOU_TYPE.SEND) { | ||
return Navigation.navigate(ROUTES.getIouSendCurrencyRoute(this.props.reportID)); | ||
} | ||
return Navigation.navigate(ROUTES.getIouRequestCurrencyRoute(this.props.reportID)); | ||
} | ||
|
||
render() { | ||
const formattedAmount = this.replaceAllDigits(this.state.amount, this.props.toLocaleDigit); | ||
|
||
return ( | ||
<> | ||
<View style={[ | ||
|
@@ -217,32 +215,14 @@ class IOUAmountPage extends React.Component { | |
styles.justifyContentCenter, | ||
]} | ||
> | ||
<TouchableOpacity onPress={() => { | ||
if (this.props.hasMultipleParticipants) { | ||
return Navigation.navigate(ROUTES.getIouBillCurrencyRoute(this.props.reportID)); | ||
} | ||
if (this.props.iouType === CONST.IOU.IOU_TYPE.SEND) { | ||
return Navigation.navigate(ROUTES.getIouSendCurrencyRoute(this.props.reportID)); | ||
} | ||
return Navigation.navigate(ROUTES.getIouRequestCurrencyRoute(this.props.reportID)); | ||
}} | ||
> | ||
<Text style={styles.iouAmountText}> | ||
{lodashGet(this.props.currencyList, [this.props.iou.selectedCurrencyCode, 'symbol'])} | ||
</Text> | ||
</TouchableOpacity> | ||
<TextInput | ||
disableKeyboard | ||
autoGrow | ||
hideFocusedState | ||
inputStyle={[styles.iouAmountTextInput, styles.p0, styles.noLeftBorderRadius, styles.noRightBorderRadius]} | ||
textInputContainerStyles={[styles.borderNone, styles.noLeftBorderRadius, styles.noRightBorderRadius]} | ||
onChangeText={this.updateAmount} | ||
ref={el => this.textInput = el} | ||
value={formattedAmount} | ||
<TextInputWithCurrencySymbol | ||
formattedAmount={formattedAmount} | ||
onChangeAmount={this.updateAmount} | ||
onCurrencyButtonPress={this.navigateToCurrencySelectionPage} | ||
placeholder={this.props.numberFormat(0)} | ||
keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} | ||
blurOnSubmit={false} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hello 👋 While refactoring this code, @mdneyazahmad can you please follow up with a PR to fix this regression, thank you! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yeah it seems like this got missed while merging main. Good catch. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great catch, the PR is up here #9351 Would any one of you @rushatgabhane or @parasharrajat be able to give this a quick test based on the PR Rushat linked to make sure the behaviour works fine and approve the PR, I will merge. Thanks! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Merged, thanks! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I missed it while fixing the merge conflict. Thanks @rushatgabhane for catching it early and @mountiny for creating the PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No worries, this is easy to miss. |
||
preferredLocale={this.props.preferredLocale} | ||
ref={el => this.textInput = el} | ||
selectedCurrencyCode={this.props.iou.selectedCurrencyCode} | ||
/> | ||
</View> | ||
<View style={[styles.w100, styles.justifyContentEnd]}> | ||
|
@@ -273,7 +253,6 @@ IOUAmountPage.defaultProps = defaultProps; | |
export default compose( | ||
withLocalize, | ||
withOnyx({ | ||
currencyList: {key: ONYXKEYS.CURRENCY_LIST}, | ||
iou: {key: ONYXKEYS.IOU}, | ||
}), | ||
)(IOUAmountPage); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import _ from 'underscore'; | ||
import * as CurrencySymbolUtils from '../../src/libs/CurrencySymbolUtils'; | ||
|
||
// This file can get outdated. In that case, you can follow these steps to update it: | ||
// - in src/libs/API.js | ||
// - call: GetCurrencyList().then(data => console.log(data.currencyList)); | ||
// - copy the json from console and format it to valid json using some external tool | ||
// - update currencyList.json | ||
import currencyList from './currencyList.json'; | ||
|
||
const currencyCodeList = _.keys(currencyList); | ||
const AVAILABLE_LOCALES = ['en', 'es']; | ||
|
||
// Contains item [isLeft, locale, currencyCode] | ||
const symbolPositions = [ | ||
[true, 'en', 'USD'], | ||
[false, 'es', 'USD'], | ||
]; | ||
|
||
describe('CurrencySymbolUtils', () => { | ||
describe('getLocalizedCurrencySymbol', () => { | ||
test.each(AVAILABLE_LOCALES)('Returns non empty string for all currencyCode with preferredLocale %s', (prefrredLocale) => { | ||
_.forEach(currencyCodeList, (currencyCode) => { | ||
const localizedSymbol = CurrencySymbolUtils.getLocalizedCurrencySymbol(prefrredLocale, currencyCode); | ||
|
||
expect(localizedSymbol).toBeTruthy(); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('isCurrencySymbolLTR', () => { | ||
test.each(symbolPositions)('Returns %s for preferredLocale %s and currencyCode %s', (isLeft, locale, currencyCode) => { | ||
const isSymbolLeft = CurrencySymbolUtils.isCurrencySymbolLTR(locale, currencyCode); | ||
expect(isSymbolLeft).toBe(isLeft); | ||
}); | ||
}); | ||
}); | ||
|
Uh oh!
There was an error while loading. Please reload this page.