Skip to content

Commit 52c7907

Browse files
authored
Merge pull request #8299 from mdneyazahmad/fix/7915-localize-currency-symbol
[Fix] Localize currency symbol
2 parents d0a9887 + d87f569 commit 52c7907

8 files changed

+1113
-42
lines changed

src/components/AmountTextInput.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import TextInput from './TextInput';
4+
import styles from '../styles/styles';
5+
import CONST from '../CONST';
6+
7+
const propTypes = {
8+
/** Formatted amount in local currency */
9+
formattedAmount: PropTypes.string.isRequired,
10+
11+
/** A ref to forward to amount text input */
12+
forwardedRef: PropTypes.oneOfType([
13+
PropTypes.func,
14+
PropTypes.shape({current: PropTypes.instanceOf(React.Component)}),
15+
]),
16+
17+
/** Function to call when amount in text input is changed */
18+
onChangeAmount: PropTypes.func.isRequired,
19+
20+
/** Placeholder value for amount text input */
21+
placeholder: PropTypes.string.isRequired,
22+
};
23+
24+
const defaultProps = {
25+
forwardedRef: undefined,
26+
};
27+
28+
function AmountTextInput(props) {
29+
return (
30+
<TextInput
31+
disableKeyboard
32+
autoGrow
33+
hideFocusedState
34+
inputStyle={[styles.iouAmountTextInput, styles.p0, styles.noLeftBorderRadius, styles.noRightBorderRadius]}
35+
textInputContainerStyles={[styles.borderNone, styles.noLeftBorderRadius, styles.noRightBorderRadius]}
36+
onChangeText={props.onChangeAmount}
37+
ref={props.forwardedRef}
38+
value={props.formattedAmount}
39+
placeholder={props.placeholder}
40+
keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD}
41+
/>
42+
);
43+
}
44+
45+
AmountTextInput.propTypes = propTypes;
46+
AmountTextInput.defaultProps = defaultProps;
47+
AmountTextInput.displayName = 'AmountTextInput';
48+
49+
export default React.forwardRef((props, ref) => (
50+
// eslint-disable-next-line react/jsx-props-no-spreading
51+
<AmountTextInput {...props} forwardedRef={ref} />
52+
));
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react';
2+
import {TouchableOpacity} from 'react-native';
3+
import PropTypes from 'prop-types';
4+
import Text from './Text';
5+
import styles from '../styles/styles';
6+
7+
const propTypes = {
8+
/** Currency symbol of selected currency */
9+
currencySymbol: PropTypes.string.isRequired,
10+
11+
/** Function to call when currency button is pressed */
12+
onCurrencyButtonPress: PropTypes.func.isRequired,
13+
};
14+
15+
function CurrencySymbolButton(props) {
16+
return (
17+
<TouchableOpacity onPress={props.onCurrencyButtonPress}>
18+
<Text style={styles.iouAmountText}>{props.currencySymbol}</Text>
19+
</TouchableOpacity>
20+
);
21+
}
22+
23+
CurrencySymbolButton.propTypes = propTypes;
24+
CurrencySymbolButton.displayName = 'CurrencySymbolButton';
25+
26+
export default CurrencySymbolButton;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import AmountTextInput from './AmountTextInput';
4+
import CurrencySymbolButton from './CurrencySymbolButton';
5+
import * as CurrencySymbolUtils from '../libs/CurrencySymbolUtils';
6+
7+
const propTypes = {
8+
/** A ref to forward to amount text input */
9+
forwardedRef: PropTypes.oneOfType([
10+
PropTypes.func,
11+
PropTypes.shape({current: PropTypes.instanceOf(React.Component)}),
12+
]),
13+
14+
/** Formatted amount in local currency */
15+
formattedAmount: PropTypes.string.isRequired,
16+
17+
/** Function to call when amount in text input is changed */
18+
onChangeAmount: PropTypes.func,
19+
20+
/** Function to call when currency button is pressed */
21+
onCurrencyButtonPress: PropTypes.func,
22+
23+
/** Placeholder value for amount text input */
24+
placeholder: PropTypes.string.isRequired,
25+
26+
/** Preferred locale of the user */
27+
preferredLocale: PropTypes.string.isRequired,
28+
29+
/** Currency code of user's selected currency */
30+
selectedCurrencyCode: PropTypes.string.isRequired,
31+
};
32+
33+
const defaultProps = {
34+
forwardedRef: undefined,
35+
onChangeAmount: () => {},
36+
onCurrencyButtonPress: () => {},
37+
};
38+
39+
function TextInputWithCurrencySymbol(props) {
40+
const currencySymbol = CurrencySymbolUtils.getLocalizedCurrencySymbol(props.preferredLocale, props.selectedCurrencyCode);
41+
const isCurrencySymbolLTR = CurrencySymbolUtils.isCurrencySymbolLTR(props.preferredLocale, props.selectedCurrencyCode);
42+
43+
const currencySymbolButton = (
44+
<CurrencySymbolButton
45+
currencySymbol={currencySymbol}
46+
onCurrencyButtonPress={props.onCurrencyButtonPress}
47+
/>
48+
);
49+
50+
const amountTextInput = (
51+
<AmountTextInput
52+
formattedAmount={props.formattedAmount}
53+
onChangeAmount={props.onChangeAmount}
54+
placeholder={props.placeholder}
55+
ref={props.forwardedRef}
56+
/>
57+
);
58+
59+
if (isCurrencySymbolLTR) {
60+
return (
61+
<>
62+
{currencySymbolButton}
63+
{amountTextInput}
64+
</>
65+
);
66+
}
67+
68+
return (
69+
<>
70+
{amountTextInput}
71+
{currencySymbolButton}
72+
</>
73+
);
74+
}
75+
76+
TextInputWithCurrencySymbol.propTypes = propTypes;
77+
TextInputWithCurrencySymbol.defaultProps = defaultProps;
78+
TextInputWithCurrencySymbol.displayName = 'TextInputWithCurrencySymbol';
79+
80+
export default React.forwardRef((props, ref) => (
81+
// eslint-disable-next-line react/jsx-props-no-spreading
82+
<TextInputWithCurrencySymbol {...props} forwardedRef={ref} />
83+
));

src/libs/CurrencySymbolUtils.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import _ from 'underscore';
2+
import * as NumberFormatUtils from './NumberFormatUtils';
3+
4+
/**
5+
* Get localized currency symbol for currency(ISO 4217) Code
6+
* @param {String} preferredLocale
7+
* @param {String} currencyCode
8+
* @returns {String}
9+
*/
10+
function getLocalizedCurrencySymbol(preferredLocale, currencyCode) {
11+
const parts = NumberFormatUtils.formatToParts(preferredLocale, 0, {
12+
style: 'currency',
13+
currency: currencyCode,
14+
});
15+
return _.find(parts, part => part.type === 'currency').value;
16+
}
17+
18+
/**
19+
* Whether the currency symbol is left-to-right.
20+
* @param {String} preferredLocale
21+
* @param {String} currencyCode
22+
* @returns {Boolean}
23+
*/
24+
function isCurrencySymbolLTR(preferredLocale, currencyCode) {
25+
const parts = NumberFormatUtils.formatToParts(preferredLocale, 0, {
26+
style: 'currency',
27+
currency: currencyCode,
28+
});
29+
30+
// Currency is LTR when the first part is of currency type.
31+
return parts[0].type === 'currency';
32+
}
33+
34+
export {
35+
getLocalizedCurrencySymbol,
36+
isCurrencySymbolLTR,
37+
};

src/pages/iou/IOUCurrencySelection.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import KeyboardAvoidingView from '../../components/KeyboardAvoidingView';
2020
import Button from '../../components/Button';
2121
import FixedFooter from '../../components/FixedFooter';
2222
import * as IOU from '../../libs/actions/IOU';
23+
import * as CurrencySymbolUtils from '../../libs/CurrencySymbolUtils';
2324
import {withNetwork} from '../../components/OnyxProvider';
2425
import networkPropTypes from '../../components/networkPropTypes';
2526

@@ -127,7 +128,7 @@ class IOUCurrencySelection extends Component {
127128
getCurrencyOptions() {
128129
const currencyListKeys = _.keys(this.props.currencyList);
129130
const currencyOptions = _.map(currencyListKeys, currencyCode => ({
130-
text: `${currencyCode} - ${this.props.currencyList[currencyCode].symbol}`,
131+
text: `${currencyCode} - ${CurrencySymbolUtils.getLocalizedCurrencySymbol(this.props.preferredLocale, currencyCode)}`,
131132
searchText: `${currencyCode} ${this.props.currencyList[currencyCode].symbol}`,
132133
currencyCode,
133134
}));

src/pages/iou/steps/IOUAmountPage.js

Lines changed: 20 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React from 'react';
22
import {
33
View,
4-
TouchableOpacity,
54
InteractionManager,
65
} from 'react-native';
76
import PropTypes from 'prop-types';
@@ -16,10 +15,9 @@ import ROUTES from '../../../ROUTES';
1615
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
1716
import compose from '../../../libs/compose';
1817
import Button from '../../../components/Button';
19-
import Text from '../../../components/Text';
2018
import CONST from '../../../CONST';
21-
import TextInput from '../../../components/TextInput';
2219
import canUseTouchScreen from '../../../libs/canUseTouchscreen';
20+
import TextInputWithCurrencySymbol from '../../../components/TextInputWithCurrencySymbol';
2321

2422
const propTypes = {
2523
/** Whether or not this IOU has multiple participants */
@@ -31,18 +29,6 @@ const propTypes = {
3129
/** Callback to inform parent modal of success */
3230
onStepComplete: PropTypes.func.isRequired,
3331

34-
/** The currency list constant object from Onyx */
35-
currencyList: PropTypes.objectOf(PropTypes.shape({
36-
/** Symbol for the currency */
37-
symbol: PropTypes.string,
38-
39-
/** Name of the currency */
40-
name: PropTypes.string,
41-
42-
/** ISO4217 Code for the currency */
43-
ISO4217: PropTypes.string,
44-
})).isRequired,
45-
4632
/** Previously selected amount to show if the user comes back to this screen */
4733
selectedAmount: PropTypes.string.isRequired,
4834

@@ -75,6 +61,7 @@ class IOUAmountPage extends React.Component {
7561
this.updateAmount = this.updateAmount.bind(this);
7662
this.stripCommaFromAmount = this.stripCommaFromAmount.bind(this);
7763
this.focusTextInput = this.focusTextInput.bind(this);
64+
this.navigateToCurrencySelectionPage = this.navigateToCurrencySelectionPage.bind(this);
7865

7966
this.state = {
8067
amount: props.selectedAmount,
@@ -205,8 +192,19 @@ class IOUAmountPage extends React.Component {
205192
.value();
206193
}
207194

195+
navigateToCurrencySelectionPage() {
196+
if (this.props.hasMultipleParticipants) {
197+
return Navigation.navigate(ROUTES.getIouBillCurrencyRoute(this.props.reportID));
198+
}
199+
if (this.props.iouType === CONST.IOU.IOU_TYPE.SEND) {
200+
return Navigation.navigate(ROUTES.getIouSendCurrencyRoute(this.props.reportID));
201+
}
202+
return Navigation.navigate(ROUTES.getIouRequestCurrencyRoute(this.props.reportID));
203+
}
204+
208205
render() {
209206
const formattedAmount = this.replaceAllDigits(this.state.amount, this.props.toLocaleDigit);
207+
210208
return (
211209
<>
212210
<View style={[
@@ -217,32 +215,14 @@ class IOUAmountPage extends React.Component {
217215
styles.justifyContentCenter,
218216
]}
219217
>
220-
<TouchableOpacity onPress={() => {
221-
if (this.props.hasMultipleParticipants) {
222-
return Navigation.navigate(ROUTES.getIouBillCurrencyRoute(this.props.reportID));
223-
}
224-
if (this.props.iouType === CONST.IOU.IOU_TYPE.SEND) {
225-
return Navigation.navigate(ROUTES.getIouSendCurrencyRoute(this.props.reportID));
226-
}
227-
return Navigation.navigate(ROUTES.getIouRequestCurrencyRoute(this.props.reportID));
228-
}}
229-
>
230-
<Text style={styles.iouAmountText}>
231-
{lodashGet(this.props.currencyList, [this.props.iou.selectedCurrencyCode, 'symbol'])}
232-
</Text>
233-
</TouchableOpacity>
234-
<TextInput
235-
disableKeyboard
236-
autoGrow
237-
hideFocusedState
238-
inputStyle={[styles.iouAmountTextInput, styles.p0, styles.noLeftBorderRadius, styles.noRightBorderRadius]}
239-
textInputContainerStyles={[styles.borderNone, styles.noLeftBorderRadius, styles.noRightBorderRadius]}
240-
onChangeText={this.updateAmount}
241-
ref={el => this.textInput = el}
242-
value={formattedAmount}
218+
<TextInputWithCurrencySymbol
219+
formattedAmount={formattedAmount}
220+
onChangeAmount={this.updateAmount}
221+
onCurrencyButtonPress={this.navigateToCurrencySelectionPage}
243222
placeholder={this.props.numberFormat(0)}
244-
keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD}
245-
blurOnSubmit={false}
223+
preferredLocale={this.props.preferredLocale}
224+
ref={el => this.textInput = el}
225+
selectedCurrencyCode={this.props.iou.selectedCurrencyCode}
246226
/>
247227
</View>
248228
<View style={[styles.w100, styles.justifyContentEnd]}>
@@ -273,7 +253,6 @@ IOUAmountPage.defaultProps = defaultProps;
273253
export default compose(
274254
withLocalize,
275255
withOnyx({
276-
currencyList: {key: ONYXKEYS.CURRENCY_LIST},
277256
iou: {key: ONYXKEYS.IOU},
278257
}),
279258
)(IOUAmountPage);

tests/unit/CurrencySymbolUtilsTest.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import _ from 'underscore';
2+
import * as CurrencySymbolUtils from '../../src/libs/CurrencySymbolUtils';
3+
4+
// This file can get outdated. In that case, you can follow these steps to update it:
5+
// - in src/libs/API.js
6+
// - call: GetCurrencyList().then(data => console.log(data.currencyList));
7+
// - copy the json from console and format it to valid json using some external tool
8+
// - update currencyList.json
9+
import currencyList from './currencyList.json';
10+
11+
const currencyCodeList = _.keys(currencyList);
12+
const AVAILABLE_LOCALES = ['en', 'es'];
13+
14+
// Contains item [isLeft, locale, currencyCode]
15+
const symbolPositions = [
16+
[true, 'en', 'USD'],
17+
[false, 'es', 'USD'],
18+
];
19+
20+
describe('CurrencySymbolUtils', () => {
21+
describe('getLocalizedCurrencySymbol', () => {
22+
test.each(AVAILABLE_LOCALES)('Returns non empty string for all currencyCode with preferredLocale %s', (prefrredLocale) => {
23+
_.forEach(currencyCodeList, (currencyCode) => {
24+
const localizedSymbol = CurrencySymbolUtils.getLocalizedCurrencySymbol(prefrredLocale, currencyCode);
25+
26+
expect(localizedSymbol).toBeTruthy();
27+
});
28+
});
29+
});
30+
31+
describe('isCurrencySymbolLTR', () => {
32+
test.each(symbolPositions)('Returns %s for preferredLocale %s and currencyCode %s', (isLeft, locale, currencyCode) => {
33+
const isSymbolLeft = CurrencySymbolUtils.isCurrencySymbolLTR(locale, currencyCode);
34+
expect(isSymbolLeft).toBe(isLeft);
35+
});
36+
});
37+
});
38+

0 commit comments

Comments
 (0)