diff --git a/src/components/AddressForm.js b/src/components/AddressForm.js index 68d451e5c7c8..29dfa29fde02 100644 --- a/src/components/AddressForm.js +++ b/src/components/AddressForm.js @@ -67,7 +67,7 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS const styles = useThemeStyles(); const {translate} = useLocalize(); const zipSampleFormat = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, [country, 'samples'], ''); - const zipFormat = translate('common.zipCodeExampleFormat', {zipSampleFormat}); + const zipFormat = ['common.zipCodeExampleFormat', {zipSampleFormat}]; const isUSAForm = country === CONST.COUNTRY.US; /** diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index 8016f1b2ea39..9b4254a9bc45 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -1,6 +1,7 @@ import type {RefObject} from 'react'; import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, View, ViewStyle} from 'react-native'; import type {Place} from 'react-native-google-places-autocomplete'; +import type {MaybePhraseKey} from '@libs/Localize'; import type Locale from '@src/types/onyx/Locale'; type CurrentLocationButtonProps = { @@ -43,7 +44,7 @@ type AddressSearchProps = { onBlur?: () => void; /** Error text to display */ - errorText?: string; + errorText?: MaybePhraseKey; /** Hint text to display */ hint?: string; diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index 010d074d1da6..f55db3dd0620 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -421,7 +421,7 @@ function AvatarWithImagePicker({ {errorData.validationError && ( )} diff --git a/src/components/CheckboxWithLabel.tsx b/src/components/CheckboxWithLabel.tsx index 602fb154deba..2919debe9cb1 100644 --- a/src/components/CheckboxWithLabel.tsx +++ b/src/components/CheckboxWithLabel.tsx @@ -3,6 +3,7 @@ import React, {useState} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {MaybePhraseKey} from '@libs/Localize'; import variables from '@styles/variables'; import Checkbox from './Checkbox'; import FormHelpMessage from './FormHelpMessage'; @@ -40,7 +41,7 @@ type CheckboxWithLabelProps = RequiredLabelProps & { style?: StyleProp; /** Error text to display */ - errorText?: string; + errorText?: MaybePhraseKey; /** Value for checkbox. This prop is intended to be set by FormProvider only */ value?: boolean; diff --git a/src/components/CountrySelector.tsx b/src/components/CountrySelector.tsx index 50a789638c94..25dc99459064 100644 --- a/src/components/CountrySelector.tsx +++ b/src/components/CountrySelector.tsx @@ -3,6 +3,7 @@ import type {ForwardedRef} from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {MaybePhraseKey} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import type {Country} from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -11,7 +12,7 @@ import MenuItemWithTopDescription from './MenuItemWithTopDescription'; type CountrySelectorProps = { /** Form error text. e.g when no country is selected */ - errorText?: string; + errorText?: MaybePhraseKey; /** Callback called when the country changes. */ onInputChange: (value?: string) => void; diff --git a/src/components/DistanceRequest/index.js b/src/components/DistanceRequest/index.js index b63ce337a1d9..eafc36a57927 100644 --- a/src/components/DistanceRequest/index.js +++ b/src/components/DistanceRequest/index.js @@ -183,7 +183,7 @@ function DistanceRequest({transactionID, report, transaction, route, isEditingRe } if (_.size(validatedWaypoints) < 2) { - return {0: translate('iou.error.atLeastTwoDifferentWaypoints')}; + return {0: 'iou.error.atLeastTwoDifferentWaypoints'}; } }; diff --git a/src/components/DotIndicatorMessage.tsx b/src/components/DotIndicatorMessage.tsx index d2143f5b48da..3765d1e3b168 100644 --- a/src/components/DotIndicatorMessage.tsx +++ b/src/components/DotIndicatorMessage.tsx @@ -35,8 +35,8 @@ type DotIndicatorMessageProps = { }; /** Check if the error includes a receipt. */ -function isReceiptError(message: string | ReceiptError): message is ReceiptError { - if (typeof message === 'string') { +function isReceiptError(message: Localize.MaybePhraseKey | ReceiptError): message is ReceiptError { + if (typeof message === 'string' || Array.isArray(message)) { return false; } return (message?.error ?? '') === CONST.IOU.RECEIPT_ERROR; @@ -57,7 +57,7 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndica .map((key) => messages[key]); // Removing duplicates using Set and transforming the result into an array - const uniqueMessages = [...new Set(sortedMessages)].map((message) => Localize.translateIfPhraseKey(message)); + const uniqueMessages = [...new Set(sortedMessages)].map((message) => (isReceiptError(message) ? message : Localize.translateIfPhraseKey(message))); const isErrorMessage = type === 'error'; diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx index 789f1cd2466d..9968bb0e0772 100644 --- a/src/components/FormAlertWithSubmitButton.tsx +++ b/src/components/FormAlertWithSubmitButton.tsx @@ -2,12 +2,13 @@ import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {MaybePhraseKey} from '@libs/Localize'; import Button from './Button'; import FormAlertWrapper from './FormAlertWrapper'; type FormAlertWithSubmitButtonProps = { /** Error message to display above button */ - message?: string | null; + message?: MaybePhraseKey; /** Whether the button is disabled */ isDisabled?: boolean; diff --git a/src/components/FormAlertWrapper.tsx b/src/components/FormAlertWrapper.tsx index bdd5622f7aeb..d8b379208a29 100644 --- a/src/components/FormAlertWrapper.tsx +++ b/src/components/FormAlertWrapper.tsx @@ -4,6 +4,7 @@ import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {MaybePhraseKey} from '@libs/Localize'; import type Network from '@src/types/onyx/Network'; import FormHelpMessage from './FormHelpMessage'; import {withNetwork} from './OnyxProvider'; @@ -28,7 +29,7 @@ type FormAlertWrapperProps = { isMessageHtml?: boolean; /** Error message to display above button */ - message?: string | null; + message?: MaybePhraseKey; /** Props to detect online status */ network: Network; @@ -68,7 +69,7 @@ function FormAlertWrapper({ {` ${translate('common.inTheFormBeforeContinuing')}.`} ); - } else if (isMessageHtml) { + } else if (isMessageHtml && typeof message === 'string') { content = ${message}`} />; } diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index 1e2e57a0b3fb..46c96fd706a9 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -7,6 +7,7 @@ import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; +import type {MaybePhraseKey} from '@libs/Localize'; import * as ValidationUtils from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import FormHelpMessage from './FormHelpMessage'; @@ -32,7 +33,7 @@ type MagicCodeInputProps = { shouldDelayFocus?: boolean; /** Error text to display */ - errorText?: string; + errorText?: MaybePhraseKey; /** Specifies autocomplete hints for the system, so it can provide autofill */ autoComplete: AutoCompleteVariant; diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 55f1cef69124..6163fa116561 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -14,6 +14,7 @@ import ControlSelection from '@libs/ControlSelection'; import convertToLTR from '@libs/convertToLTR'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getButtonState from '@libs/getButtonState'; +import type {MaybePhraseKey} from '@libs/Localize'; import type {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; import * as Session from '@userActions/Session'; @@ -136,7 +137,7 @@ type MenuItemProps = (IconProps | AvatarProps | NoIcon) & { error?: string; /** Error to display at the bottom of the component */ - errorText?: string; + errorText?: MaybePhraseKey; /** A boolean flag that gives the icon a green fill if true */ success?: boolean; diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index faa487887f22..92656a7ad225 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -567,7 +567,7 @@ function MoneyRequestConfirmationList(props) { )} {button} @@ -586,7 +586,6 @@ function MoneyRequestConfirmationList(props) { formError, styles.ph1, styles.mb2, - translate, ]); const {image: receiptImage, thumbnail: receiptThumbnail} = diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 8a61fe6daec5..a2f79d2696b8 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -616,13 +616,13 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ )} {button} ); - }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2, translate]); + }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); const {image: receiptImage, thumbnail: receiptThumbnail} = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : {}; return ( diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 975d154b885b..2c41864564a3 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -1,9 +1,12 @@ +import {mapValues} from 'lodash'; import React, {useCallback} from 'react'; import type {ImageStyle, StyleProp, TextStyle, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import type {MaybePhraseKey} from '@libs/Localize'; import mapChildrenFlat from '@libs/mapChildrenFlat'; import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; import CONST from '@src/CONST'; @@ -59,6 +62,10 @@ type OfflineWithFeedbackProps = ChildrenProps & { type StrikethroughProps = Partial & {style: Array}; +function isMaybePhraseKeyType(message: unknown): message is MaybePhraseKey { + return typeof message === 'string' || Array.isArray(message); +} + function OfflineWithFeedback({ pendingAction, canDismissError = true, @@ -82,8 +89,8 @@ function OfflineWithFeedback({ // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. const errorEntries = Object.entries(errors ?? {}); - const filteredErrorEntries = errorEntries.filter((errorEntry): errorEntry is [string, string | ReceiptError] => errorEntry[1] !== null); - const errorMessages = Object.fromEntries(filteredErrorEntries); + const filteredErrorEntries = errorEntries.filter((errorEntry): errorEntry is [string, MaybePhraseKey | ReceiptError] => errorEntry[1] !== null); + const errorMessages = mapValues(Object.fromEntries(filteredErrorEntries), (error) => (isMaybePhraseKeyType(error) ? ErrorUtils.getErrorMessageWithTranslationData(error) : error)); const hasErrorMessages = !isEmptyObject(errorMessages); const isOfflinePendingAction = !!isOffline && !!pendingAction; diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index bb1732ceb2f8..cb6a2dcbe722 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -253,7 +253,7 @@ class BaseOptionsSelector extends Component { updateSearchValue(value) { this.setState({ paginationPage: 1, - errorMessage: value.length > this.props.maxLength ? this.props.translate('common.error.characterLimitExceedCounter', {length: value.length, limit: this.props.maxLength}) : '', + errorMessage: value.length > this.props.maxLength ? ['common.error.characterLimitExceedCounter', {length: value.length, limit: this.props.maxLength}] : '', value, }); diff --git a/src/components/PDFView/PDFPasswordForm.js b/src/components/PDFView/PDFPasswordForm.js index 42d2ebbb771e..10596bb9faf9 100644 --- a/src/components/PDFView/PDFPasswordForm.js +++ b/src/components/PDFView/PDFPasswordForm.js @@ -55,13 +55,13 @@ function PDFPasswordForm({isFocused, isPasswordInvalid, shouldShowLoadingIndicat const errorText = useMemo(() => { if (isPasswordInvalid) { - return translate('attachmentView.passwordIncorrect'); + return 'attachmentView.passwordIncorrect'; } if (!_.isEmpty(validationErrorText)) { - return translate(validationErrorText); + return validationErrorText; } return ''; - }, [isPasswordInvalid, translate, validationErrorText]); + }, [isPasswordInvalid, validationErrorText]); useEffect(() => { if (!isFocused) { diff --git a/src/components/Picker/types.ts b/src/components/Picker/types.ts index edf39a59c9d8..a12f4cbe683a 100644 --- a/src/components/Picker/types.ts +++ b/src/components/Picker/types.ts @@ -1,5 +1,6 @@ import type {ChangeEvent, Component, ReactElement} from 'react'; import type {MeasureLayoutOnSuccessCallback, NativeMethods, StyleProp, ViewStyle} from 'react-native'; +import type {MaybePhraseKey} from '@libs/Localize'; type MeasureLayoutOnFailCallback = () => void; @@ -58,7 +59,7 @@ type BasePickerProps = { placeholder?: PickerPlaceholder; /** Error text to display */ - errorText?: string; + errorText?: MaybePhraseKey; /** Customize the BasePicker container */ containerStyles?: StyleProp; diff --git a/src/components/RadioButtonWithLabel.tsx b/src/components/RadioButtonWithLabel.tsx index f4bad4c082a7..52464a1453a1 100644 --- a/src/components/RadioButtonWithLabel.tsx +++ b/src/components/RadioButtonWithLabel.tsx @@ -3,6 +3,7 @@ import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {MaybePhraseKey} from '@libs/Localize'; import FormHelpMessage from './FormHelpMessage'; import * as Pressables from './Pressable'; import RadioButton from './RadioButton'; @@ -28,7 +29,7 @@ type RadioButtonWithLabelProps = { hasError?: boolean; /** Error text to display */ - errorText?: string; + errorText?: MaybePhraseKey; }; const PressableWithFeedback = Pressables.PressableWithFeedback; diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index 60be8430b056..f634c6e0b3d6 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import refPropTypes from '@components/refPropTypes'; +import {translatableTextPropTypes} from '@libs/Localize'; const propTypes = { /** Callback to execute when the text input is modified correctly */ @@ -12,7 +13,7 @@ const propTypes = { disabled: PropTypes.bool, /** Error text to show */ - errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + errorText: translatableTextPropTypes, /** A ref forwarded to the TextInput */ forwardedRef: refPropTypes, diff --git a/src/components/StatePicker/index.tsx b/src/components/StatePicker/index.tsx index a03e4f15fba0..b00111319b4a 100644 --- a/src/components/StatePicker/index.tsx +++ b/src/components/StatePicker/index.tsx @@ -6,13 +6,14 @@ import FormHelpMessage from '@components/FormHelpMessage'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {MaybePhraseKey} from '@libs/Localize'; import type {CountryData} from '@libs/searchCountryOptions'; import StateSelectorModal from './StateSelectorModal'; import type {State} from './StateSelectorModal'; type StatePickerProps = { /** Error text to display */ - errorText?: string; + errorText?: MaybePhraseKey; /** State to display */ value?: State; diff --git a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js index 78f06b4075e0..e6077bde71b3 100644 --- a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import sourcePropTypes from '@components/Image/sourcePropTypes'; +import {translatableTextPropTypes} from '@libs/Localize'; const propTypes = { /** Input label */ @@ -18,7 +19,7 @@ const propTypes = { placeholder: PropTypes.string, /** Error text to display */ - errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + errorText: translatableTextPropTypes, /** Icon to display in right side of text input */ icon: sourcePropTypes, @@ -68,7 +69,7 @@ const propTypes = { maxLength: PropTypes.number, /** Hint text to display below the TextInput */ - hint: PropTypes.string, + hint: translatableTextPropTypes, /** Prefix character */ prefixCharacter: PropTypes.string, diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 01400adb0440..a637dc22d72e 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -67,7 +67,7 @@ type CustomBaseTextInputProps = { hideFocusedState?: boolean; /** Hint text to display below the TextInput */ - hint?: string; + hint?: MaybePhraseKey; /** Prefix character */ prefixCharacter?: string; diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.js index 4664251ca765..f57f2540dfb3 100644 --- a/src/components/TimePicker/TimePicker.js +++ b/src/components/TimePicker/TimePicker.js @@ -536,7 +536,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { {isError ? ( ) : ( diff --git a/src/components/ValuePicker/index.js b/src/components/ValuePicker/index.js index d90529114af4..28fa1ab26af2 100644 --- a/src/components/ValuePicker/index.js +++ b/src/components/ValuePicker/index.js @@ -7,12 +7,13 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import refPropTypes from '@components/refPropTypes'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import {translatableTextPropTypes} from '@libs/Localize'; import variables from '@styles/variables'; import ValueSelectorModal from './ValueSelectorModal'; const propTypes = { /** Form Error description */ - errorText: PropTypes.string, + errorText: translatableTextPropTypes, /** Item to display */ value: PropTypes.string, diff --git a/src/components/transactionPropTypes.js b/src/components/transactionPropTypes.js index c70a2e524583..bdcf60bec5da 100644 --- a/src/components/transactionPropTypes.js +++ b/src/components/transactionPropTypes.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import _ from 'underscore'; +import {translatableTextPropTypes} from '@libs/Localize'; import CONST from '@src/CONST'; import sourcePropTypes from './Image/sourcePropTypes'; @@ -80,5 +81,5 @@ export default PropTypes.shape({ }), /** Server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), }); diff --git a/src/languages/en.ts b/src/languages/en.ts index 3eb9f4ca6e7f..8816d1a0f9c7 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -904,6 +904,9 @@ export default { sharedNoteMessage: 'Keep notes about this chat here. Expensify employees and other users on the team.expensify.com domain can view these notes.', composerLabel: 'Notes', myNote: 'My note', + error: { + genericFailureMessage: "Private notes couldn't be saved", + }, }, addDebitCardPage: { addADebitCard: 'Add a debit card', diff --git a/src/languages/es.ts b/src/languages/es.ts index cba4a0daba32..b425f555c972 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -899,6 +899,9 @@ export default { sharedNoteMessage: 'Guarda notas sobre este chat aquí. Los empleados de Expensify y otros usuarios del dominio team.expensify.com pueden ver estas notas.', composerLabel: 'Notas', myNote: 'Mi nota', + error: { + genericFailureMessage: 'Las notas privadas no han podido ser guardadas', + }, }, addDebitCardPage: { addADebitCard: 'Añadir una tarjeta de débito', diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index 6fbba1e750dc..8cfaa684917e 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -1,3 +1,4 @@ +import mapValues from 'lodash/mapValues'; import CONST from '@src/CONST'; import type {TranslationFlatObject, TranslationPaths} from '@src/languages/types'; import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; @@ -38,8 +39,8 @@ function getAuthenticateErrorMessage(response: Response): keyof TranslationFlatO * Method used to get an error object with microsecond as the key. * @param error - error key or message to be saved */ -function getMicroSecondOnyxError(error: string | null): Errors { - return {[DateUtils.getMicroseconds()]: error}; +function getMicroSecondOnyxError(error: string | null, isTranslated = false): Errors { + return {[DateUtils.getMicroseconds()]: error && [error, {isTranslated}]}; } /** @@ -50,11 +51,16 @@ function getMicroSecondOnyxErrorObject(error: Errors): ErrorFields { return {[DateUtils.getMicroseconds()]: error}; } +// We can assume that if error is a string, it has already been translated because it is server error +function getErrorMessageWithTranslationData(error: Localize.MaybePhraseKey): Localize.MaybePhraseKey { + return typeof error === 'string' ? [error, {isTranslated: true}] : error; +} + type OnyxDataWithErrors = { errors?: Errors | null; }; -function getLatestErrorMessage(onyxData: TOnyxData): string | null { +function getLatestErrorMessage(onyxData: TOnyxData): Localize.MaybePhraseKey { const errors = onyxData.errors ?? {}; if (Object.keys(errors).length === 0) { @@ -62,8 +68,7 @@ function getLatestErrorMessage(onyxData: T } const key = Object.keys(errors).sort().reverse()[0]; - - return errors[key]; + return getErrorMessageWithTranslationData(errors[key]); } function getLatestErrorMessageField(onyxData: TOnyxData): Errors { @@ -90,8 +95,7 @@ function getLatestErrorField(onyxData } const key = Object.keys(errorsForField).sort().reverse()[0]; - - return {[key]: errorsForField[key]}; + return {[key]: getErrorMessageWithTranslationData(errorsForField[key])}; } function getEarliestErrorField(onyxData: TOnyxData, fieldName: string): Errors { @@ -102,18 +106,33 @@ function getEarliestErrorField(onyxDa } const key = Object.keys(errorsForField).sort()[0]; - - return {[key]: errorsForField[key]}; + return {[key]: getErrorMessageWithTranslationData(errorsForField[key])}; } -type ErrorsList = Record; +/** + * Method used to attach already translated message with isTranslated property + * @param errors - An object containing current errors in the form + * @returns Errors in the form of {timestamp: [message, {isTranslated}]} + */ +function getErrorsWithTranslationData(errors: Localize.MaybePhraseKey | Errors): Errors { + if (!errors || (Array.isArray(errors) && errors.length === 0)) { + return {}; + } + + if (typeof errors === 'string' || Array.isArray(errors)) { + // eslint-disable-next-line @typescript-eslint/naming-convention + return {'0': getErrorMessageWithTranslationData(errors)}; + } + + return mapValues(errors, getErrorMessageWithTranslationData); +} /** * Method used to generate error message for given inputID * @param errors - An object containing current errors in the form * @param message - Message to assign to the inputID errors */ -function addErrorMessage(errors: ErrorsList, inputID?: string, message?: TKey | Localize.MaybePhraseKey) { +function addErrorMessage(errors: Errors, inputID?: string, message?: TKey | Localize.MaybePhraseKey) { if (!message || !inputID) { return; } @@ -138,6 +157,8 @@ export { getLatestErrorMessage, getLatestErrorField, getEarliestErrorField, + getErrorMessageWithTranslationData, + getErrorsWithTranslationData, addErrorMessage, getLatestErrorMessageField, }; diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 0df9e25eff25..7c0cd437d5f9 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -1,3 +1,4 @@ +import PropTypes from 'prop-types'; import * as RNLocalize from 'react-native-localize'; import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; @@ -98,7 +99,15 @@ function translateLocal(phrase: TKey, ...variable return translate(BaseLocaleListener.getPreferredLocale(), phrase, ...variables); } -type MaybePhraseKey = string | null | [string, Record & {isTranslated?: true}] | []; +/** + * Traslatable text with phrase key and/or variables + * Use MaybePhraseKey for Typescript + * + * E.g. ['common.error.characterLimitExceedCounter', {length: 5, limit: 20}] + */ +const translatableTextPropTypes = PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]); + +type MaybePhraseKey = string | null | [string, Record & {isTranslated?: boolean}] | []; /** * Return translated string for given error. @@ -177,5 +186,5 @@ function getDevicePreferredLocale(): string { return RNLocalize.findBestAvailableLanguage([CONST.LOCALES.EN, CONST.LOCALES.ES])?.languageTag ?? CONST.LOCALES.DEFAULT; } -export {translate, translateLocal, translateIfPhraseKey, formatList, formatMessageElementList, getDevicePreferredLocale}; +export {translatableTextPropTypes, translate, translateLocal, translateIfPhraseKey, formatList, formatMessageElementList, getDevicePreferredLocale}; export type {PhraseParameters, Phrase, MaybePhraseKey}; diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 38a421409ade..2cc32616562d 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -3,7 +3,6 @@ import type {OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; import type {ActivatePhysicalExpensifyCardParams, ReportVirtualExpensifyCardFraudParams, RequestReplacementExpensifyCardParams, RevealExpensifyCardDetailsParams} from '@libs/API/parameters'; import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; -import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Response} from '@src/types/onyx'; @@ -167,12 +166,14 @@ function revealVirtualCardDetails(cardID: number): Promise { API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.REVEAL_EXPENSIFY_CARD_DETAILS, parameters) .then((response) => { if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) { - reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure')); + // eslint-disable-next-line prefer-promise-reject-errors + reject('cardPage.cardDetailsLoadingFailure'); return; } resolve(response); }) - .catch(() => reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure'))); + // eslint-disable-next-line prefer-promise-reject-errors + .catch(() => reject('cardPage.cardDetailsLoadingFailure')); }); } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 5b4fb8160894..2d13624277f0 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2529,7 +2529,7 @@ const updatePrivateNotes = (reportID: string, accountID: number, note: string) = value: { privateNotes: { [accountID]: { - errors: ErrorUtils.getMicroSecondOnyxError("Private notes couldn't be saved"), + errors: ErrorUtils.getMicroSecondOnyxError('privateNotes.error.genericFailureMessage'), }, }, }, diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 4fbeba0abaa6..d000d5ebfbec 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -607,7 +607,7 @@ function clearAccountMessages() { } function setAccountError(error: string) { - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: ErrorUtils.getMicroSecondOnyxError(error)}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: ErrorUtils.getMicroSecondOnyxError(error, true)}); } // It's necessary to throttle requests to reauthenticate since calling this multiple times will cause Pusher to diff --git a/src/pages/EnablePayments/OnfidoPrivacy.js b/src/pages/EnablePayments/OnfidoPrivacy.js index 77b884fb2934..8de8bdb4bf07 100644 --- a/src/pages/EnablePayments/OnfidoPrivacy.js +++ b/src/pages/EnablePayments/OnfidoPrivacy.js @@ -45,9 +45,11 @@ function OnfidoPrivacy({walletOnfidoData, translate, form}) { BankAccounts.openOnfidoFlow(); }; - let onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData) || ''; + const onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData) || ''; const onfidoFixableErrors = lodashGet(walletOnfidoData, 'fixableErrors', []); - onfidoError += !_.isEmpty(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; + if (_.isArray(onfidoError)) { + onfidoError[0] += !_.isEmpty(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; + } return ( diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js index 71f8797f1a34..df95fc0a01b7 100755 --- a/src/pages/NewChatPage.js +++ b/src/pages/NewChatPage.js @@ -272,7 +272,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, translate, i shouldShowReferralCTA={!dismissedReferralBanners[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]} referralContentType={CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT} confirmButtonText={selectedOptions.length > 1 ? translate('newChatPage.createGroup') : translate('newChatPage.createChat')} - textInputAlert={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''} + textInputAlert={isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''} onConfirmSelection={createGroup} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index d1d43f6a4108..d09b03d9007f 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -106,8 +106,8 @@ function AddressForm(props) { value={props.values.street} defaultValue={props.defaultValues.street} onInputChange={props.onFieldChange} - errorText={props.errors.street ? props.translate('bankAccount.error.addressStreet') : ''} - hint={props.translate('common.noPO')} + errorText={props.errors.street ? 'bankAccount.error.addressStreet' : ''} + hint="common.noPO" renamedInputKeys={props.inputKeys} maxInputLength={CONST.FORM_CHARACTER_LIMIT} isLimitedToUSA @@ -123,7 +123,7 @@ function AddressForm(props) { value={props.values.city} defaultValue={props.defaultValues.city} onChangeText={(value) => props.onFieldChange({city: value})} - errorText={props.errors.city ? props.translate('bankAccount.error.addressCity') : ''} + errorText={props.errors.city ? 'bankAccount.error.addressCity' : ''} containerStyles={[styles.mt4]} /> @@ -135,7 +135,7 @@ function AddressForm(props) { value={props.values.state} defaultValue={props.defaultValues.state || ''} onInputChange={(value) => props.onFieldChange({state: value})} - errorText={props.errors.state ? props.translate('bankAccount.error.addressState') : ''} + errorText={props.errors.state ? 'bankAccount.error.addressState' : ''} /> props.onFieldChange({zipCode: value})} - errorText={props.errors.zipCode ? props.translate('bankAccount.error.zipCode') : ''} + errorText={props.errors.zipCode ? 'bankAccount.error.zipCode' : ''} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} - hint={props.translate('common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples})} + hint={['common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}]} containerStyles={[styles.mt2]} /> diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 47f81448a1a4..87537d05e3b5 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -220,7 +220,7 @@ function CompanyStep({reimbursementAccount, reimbursementAccountDraft, getDefaul containerStyles={[styles.mt4]} defaultValue={getDefaultStateForField('website', defaultWebsite)} shouldSaveDraft - hint={translate('common.websiteExample')} + hint="common.websiteExample" inputMode={CONST.INPUT_MODE.URL} /> @@ -161,7 +161,7 @@ function IdentityForm(props) { role={CONST.ROLE.PRESENTATION} value={props.values.lastName} defaultValue={props.defaultValues.lastName} - errorText={props.errors.lastName ? props.translate('bankAccount.error.lastName') : ''} + errorText={props.errors.lastName ? 'bankAccount.error.lastName' : ''} /> @@ -187,7 +187,7 @@ function IdentityForm(props) { containerStyles={[styles.mt4]} inputMode={CONST.INPUT_MODE.NUMERIC} defaultValue={props.defaultValues.ssnLast4} - errorText={props.errors.ssnLast4 ? props.translate('bankAccount.error.ssnLast4') : ''} + errorText={props.errors.ssnLast4 ? 'bankAccount.error.ssnLast4' : ''} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.SSN} /> { if (isAmountInvalid(currentAmount)) { - setFormError(translate('iou.error.invalidAmount')); + setFormError('iou.error.invalidAmount'); return; } if (isTaxAmountInvalid(currentAmount, taxAmount, isTaxAmountForm)) { - setFormError(translate('iou.error.invalidTaxAmount', {amount: formattedTaxAmount})); + setFormError(['iou.error.invalidTaxAmount', {amount: formattedTaxAmount}]); return; } @@ -243,7 +243,7 @@ function MoneyRequestAmountForm({amount, taxAmount, currency, isEditing, forward initializeAmount(backendAmount); onSubmitButtonPress({amount: currentAmount, currency}); - }, [onSubmitButtonPress, currentAmount, taxAmount, currency, isTaxAmountForm, formattedTaxAmount, translate, initializeAmount]); + }, [onSubmitButtonPress, currentAmount, taxAmount, currency, isTaxAmountForm, formattedTaxAmount, initializeAmount]); /** * Input handler to check for a forward-delete key (or keyboard shortcut) press. diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 7349b0c9fc84..7006c2703b13 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -97,7 +97,7 @@ function MoneyRequestParticipantsSelector({ const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); - const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; + const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; const newChatOptions = useMemo(() => { const chatOptions = OptionsListUtils.getFilteredOptions( diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 7d2a505ea757..e0f414910d7b 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -25,6 +25,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import compose from '@libs/compose'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import {translatableTextPropTypes} from '@libs/Localize'; import getTopmostSettingsCentralPaneName from '@libs/Navigation/getTopmostSettingsCentralPaneName'; import Navigation from '@libs/Navigation/Navigation'; import * as UserUtils from '@libs/UserUtils'; @@ -71,7 +72,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), }), ), diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js index 7cafbe21ff6b..a9acf37ae556 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js @@ -21,6 +21,7 @@ import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeSt import compose from '@libs/compose'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import * as ErrorUtils from '@libs/ErrorUtils'; +import {translatableTextPropTypes} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import * as Session from '@userActions/Session'; import * as User from '@userActions/User'; @@ -44,7 +45,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), /** Field-specific pending states for offline UI status */ pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js index a4119e60d860..c85d123ad3fd 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js @@ -16,6 +16,7 @@ import Text from '@components/Text'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; +import {translatableTextPropTypes} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -36,7 +37,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), /** Field-specific pending states for offline UI status */ pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js index 8f6982e24b98..69fe8490f6aa 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js @@ -15,6 +15,7 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; +import {translatableTextPropTypes} from '@libs/Localize'; import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as User from '@userActions/User'; @@ -37,7 +38,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), /** Field-specific pending states for offline UI status */ pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js index 453da6eb4c40..5c1fa30a88f1 100644 --- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js @@ -18,6 +18,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; +import {translatableTextPropTypes} from '@libs/Localize'; import * as ValidationUtils from '@libs/ValidationUtils'; import * as Session from '@userActions/Session'; import * as User from '@userActions/User'; @@ -45,7 +46,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), /** Field-specific pending states for offline UI status */ pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), @@ -188,7 +189,7 @@ function BaseValidateCodeForm(props) { name="validateCode" value={validateCode} onChangeText={onTextInput} - errorText={formError.validateCode ? props.translate(formError.validateCode) : ErrorUtils.getLatestErrorMessage(props.account)} + errorText={formError.validateCode || ErrorUtils.getLatestErrorMessage(props.account)} hasError={!_.isEmpty(validateLoginError)} onFulfill={validateAndSubmitForm} autoFocus={false} diff --git a/src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.js b/src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.js index 84ca74c2842f..61208447495d 100644 --- a/src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.js +++ b/src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.js @@ -54,21 +54,17 @@ function getSelectedStatusType(data) { } const useValidateCustomDate = (data) => { - const {translate} = useLocalize(); const [customDateError, setCustomDateError] = useState(''); const [customTimeError, setCustomTimeError] = useState(''); const validate = () => { const {dateValidationErrorKey, timeValidationErrorKey} = ValidationUtils.validateDateTimeIsAtLeastOneMinuteInFuture(data); - const dateError = dateValidationErrorKey ? translate(dateValidationErrorKey) : ''; - setCustomDateError(dateError); - - const timeError = timeValidationErrorKey ? translate(timeValidationErrorKey) : ''; - setCustomTimeError(timeError); + setCustomDateError(dateValidationErrorKey); + setCustomTimeError(timeValidationErrorKey); return { - dateError, - timeError, + dateValidationErrorKey, + timeValidationErrorKey, }; }; diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js index 34ad1e2d4c8e..966fef449da4 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.js @@ -19,6 +19,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; +import {translatableTextPropTypes} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as UserUtils from '@libs/UserUtils'; @@ -37,7 +38,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), }), ), diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js index 420d976dcd26..aafa144e769f 100644 --- a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js +++ b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js @@ -115,7 +115,7 @@ function CodesStep({account = defaultAccount, backTo}) { {!_.isEmpty(error) && ( )} diff --git a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/BaseTwoFactorAuthForm.js b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/BaseTwoFactorAuthForm.js index 901c0aa1cffd..f65f7368de76 100644 --- a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/BaseTwoFactorAuthForm.js +++ b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/BaseTwoFactorAuthForm.js @@ -93,7 +93,7 @@ function BaseTwoFactorAuthForm(props) { value={twoFactorAuthCode} onChangeText={onTextInput} onFulfill={validateAndSubmitForm} - errorText={formError.twoFactorAuthCode ? props.translate(formError.twoFactorAuthCode) : ErrorUtils.getLatestErrorMessage(props.account)} + errorText={formError.twoFactorAuthCode || ErrorUtils.getLatestErrorMessage(props.account)} ref={inputRef} autoFocus={false} /> diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.js b/src/pages/settings/Wallet/ActivatePhysicalCardPage.js index 649e42bfffbe..24156a7c74fc 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.js +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.js @@ -119,12 +119,12 @@ function ActivatePhysicalCardPage({ activateCardCodeInputRef.current.blur(); if (lastFourDigits.replace(CONST.MAGIC_CODE_EMPTY_CHAR, '').length !== LAST_FOUR_DIGITS_LENGTH) { - setFormError(translate('activateCardPage.error.thatDidntMatch')); + setFormError('activateCardPage.error.thatDidntMatch'); return; } CardSettings.activatePhysicalExpensifyCard(lastFourDigits, cardID); - }, [lastFourDigits, cardID, translate]); + }, [lastFourDigits, cardID]); if (_.isEmpty(physicalCard)) { return ; diff --git a/src/pages/settings/Wallet/AddDebitCardPage.js b/src/pages/settings/Wallet/AddDebitCardPage.js index 0c5cef489517..5cdbbd41904b 100644 --- a/src/pages/settings/Wallet/AddDebitCardPage.js +++ b/src/pages/settings/Wallet/AddDebitCardPage.js @@ -178,7 +178,7 @@ function DebitCardPage(props) { role={CONST.ROLE.PRESENTATION} inputMode={CONST.INPUT_MODE.NUMERIC} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} - hint={translate('common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples})} + hint={['common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}]} containerStyles={[styles.mt4]} /> diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js index ade598608f50..6688a0a69fa9 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js @@ -12,6 +12,7 @@ import * as Wallet from '@libs/actions/Wallet'; import * as CardUtils from '@libs/CardUtils'; import FormUtils from '@libs/FormUtils'; import * as GetPhysicalCardUtils from '@libs/GetPhysicalCardUtils'; +import {translatableTextPropTypes} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import assignedCardPropTypes from '@pages/settings/Wallet/assignedCardPropTypes'; import CONST from '@src/CONST'; @@ -69,7 +70,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), /** Field-specific pending states for offline UI status */ pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index cacf35db22a6..755790dfec81 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -18,6 +18,7 @@ import * as CardUtils from '@libs/CardUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import FormUtils from '@libs/FormUtils'; import * as GetPhysicalCardUtils from '@libs/GetPhysicalCardUtils'; +import {translatableTextPropTypes} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as Card from '@userActions/Card'; @@ -56,7 +57,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), /** Field-specific pending states for offline UI status */ pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), @@ -192,7 +193,7 @@ function ExpensifyCardPage({ ) : null} diff --git a/src/pages/settings/Wallet/ReportCardLostPage.js b/src/pages/settings/Wallet/ReportCardLostPage.js index 49b69188c377..b78c0b6bdc21 100644 --- a/src/pages/settings/Wallet/ReportCardLostPage.js +++ b/src/pages/settings/Wallet/ReportCardLostPage.js @@ -194,7 +194,7 @@ function ReportCardLostPage({ @@ -212,7 +212,7 @@ function ReportCardLostPage({ diff --git a/src/pages/settings/Wallet/TransferBalancePage.js b/src/pages/settings/Wallet/TransferBalancePage.js index 8128395965b7..3dfb1f059933 100644 --- a/src/pages/settings/Wallet/TransferBalancePage.js +++ b/src/pages/settings/Wallet/TransferBalancePage.js @@ -18,6 +18,7 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PaymentUtils from '@libs/PaymentUtils'; import userWalletPropTypes from '@pages/EnablePayments/userWalletPropTypes'; @@ -166,7 +167,7 @@ function TransferBalancePage(props) { const transferAmount = props.userWallet.currentBalance - calculatedFee; const isTransferable = transferAmount > 0; const isButtonDisabled = !isTransferable || !selectedAccount; - const errorMessage = !_.isEmpty(props.walletTransfer.errors) ? _.chain(props.walletTransfer.errors).values().first().value() : ''; + const errorMessage = ErrorUtils.getLatestErrorMessage(props.walletTransfer); const shouldShowTransferView = PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, props.bankAccountList) && diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js index 6cbef7da7f3f..a4221e8834de 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.js @@ -259,9 +259,8 @@ function LoginForm(props) { }, })); - const formErrorText = useMemo(() => (formError ? translate(formError) : ''), [formError, translate]); const serverErrorText = useMemo(() => ErrorUtils.getLatestErrorMessage(props.account), [props.account]); - const shouldShowServerError = !_.isEmpty(serverErrorText) && _.isEmpty(formErrorText); + const shouldShowServerError = !_.isEmpty(serverErrorText) && _.isEmpty(formError); return ( <> @@ -302,18 +301,17 @@ function LoginForm(props) { autoCapitalize="none" autoCorrect={false} inputMode={CONST.INPUT_MODE.EMAIL} - errorText={formErrorText} + errorText={formError || ''} hasError={shouldShowServerError} maxLength={CONST.LOGIN_CHARACTER_LIMIT} /> {!_.isEmpty(props.account.success) && {props.account.success}} {!_.isEmpty(props.closeAccount.success || props.account.message) && ( - // DotIndicatorMessage mostly expects onyxData errors, so we need to mock an object so that the messages looks similar to prop.account.errors )} { diff --git a/src/pages/signin/UnlinkLoginForm.js b/src/pages/signin/UnlinkLoginForm.js index 851a984407e1..52eb710e2ea5 100644 --- a/src/pages/signin/UnlinkLoginForm.js +++ b/src/pages/signin/UnlinkLoginForm.js @@ -13,6 +13,7 @@ import Text from '@components/Text'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; +import * as ErrorUtils from '@libs/ErrorUtils'; import * as Session from '@userActions/Session'; import redirectToSignIn from '@userActions/SignInRedirect'; import CONST from '@src/CONST'; @@ -64,18 +65,17 @@ function UnlinkLoginForm(props) { {props.translate('unlinkLoginForm.noLongerHaveAccess', {primaryLogin})} {!_.isEmpty(props.account.message) && ( - // DotIndicatorMessage mostly expects onyxData errors so we need to mock an object so that the messages looks similar to prop.account.errors )} {!_.isEmpty(props.account.errors) && ( )} diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index fd5e9b952612..4afba77b77b5 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -326,7 +326,7 @@ function BaseValidateCodeForm(props) { onChangeText={(text) => onTextInput(text, 'recoveryCode')} maxLength={CONST.RECOVERY_CODE_LENGTH} label={props.translate('recoveryCodeForm.recoveryCode')} - errorText={formError.recoveryCode ? props.translate(formError.recoveryCode) : ''} + errorText={formError.recoveryCode || ''} hasError={hasError} onSubmitEditing={validateAndSubmitForm} autoFocus @@ -342,7 +342,7 @@ function BaseValidateCodeForm(props) { onChangeText={(text) => onTextInput(text, 'twoFactorAuthCode')} onFulfill={validateAndSubmitForm} maxLength={CONST.TFA_CODE_LENGTH} - errorText={formError.twoFactorAuthCode ? props.translate(formError.twoFactorAuthCode) : ''} + errorText={formError.twoFactorAuthCode || ''} hasError={hasError} autoFocus key="twoFactorAuthCode" @@ -372,7 +372,7 @@ function BaseValidateCodeForm(props) { value={validateCode} onChangeText={(text) => onTextInput(text, 'validateCode')} onFulfill={validateAndSubmitForm} - errorText={formError.validateCode ? props.translate(formError.validateCode) : ''} + errorText={formError.validateCode || ''} hasError={hasError} autoFocus key="validateCode" diff --git a/src/pages/tasks/NewTaskPage.js b/src/pages/tasks/NewTaskPage.js index 1c4c3f58b0a1..bf54d02f778f 100644 --- a/src/pages/tasks/NewTaskPage.js +++ b/src/pages/tasks/NewTaskPage.js @@ -110,17 +110,17 @@ function NewTaskPage(props) { // the response function onSubmit() { if (!props.task.title && !props.task.shareDestination) { - setErrorMessage(props.translate('newTaskPage.confirmError')); + setErrorMessage('newTaskPage.confirmError'); return; } if (!props.task.title) { - setErrorMessage(props.translate('newTaskPage.pleaseEnterTaskName')); + setErrorMessage('newTaskPage.pleaseEnterTaskName'); return; } if (!props.task.shareDestination) { - setErrorMessage(props.translate('newTaskPage.pleaseEnterTaskDestination')); + setErrorMessage('newTaskPage.pleaseEnterTaskDestination'); return; } diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.js b/src/pages/tasks/TaskShareDestinationSelectorModal.js index 64fd5f50b61f..b8d9229e6158 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.js +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.js @@ -145,7 +145,7 @@ function TaskShareDestinationSelectorModal(props) { showTitleTooltip shouldShowOptions={didScreenTransitionEnd} textInputLabel={props.translate('optionsSelector.nameEmailOrPhoneNumber')} - textInputAlert={isOffline ? `${props.translate('common.youAppearToBeOffline')} ${props.translate('search.resultsAreLimited')}` : ''} + textInputAlert={isOffline ? [`${props.translate('common.youAppearToBeOffline')} ${props.translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} autoFocus={false} ref={inputCallbackRef} diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 4bf2bf3a8472..8fcf425d8f34 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -325,7 +325,7 @@ function WorkspaceInvitePage(props) { isAlertVisible={shouldShowAlertPrompt} buttonText={translate('common.next')} onSubmit={inviteUser} - message={props.policy.alertMessage} + message={[props.policy.alertMessage, {isTranslated: true}]} containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto, styles.mb5]} enabledWhenOffline disablePressOnEnter diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index cdcf15f8b0e2..c24ea1af4a5e 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -408,7 +408,7 @@ function WorkspaceMembersPage(props) { return ( Policy.dismissAddedWithPrimaryLoginMessages(policyID)} /> diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index 1c6981b9936a..b8676faf0510 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -23,6 +23,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; +import {translatableTextPropTypes} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -69,7 +70,7 @@ const propTypes = { isLoading: PropTypes.bool, /** Field errors in the form */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), }), /** Session details for the user */ diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index 8764412c87ad..d4397d3d5d1c 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -6,6 +6,7 @@ import React, {forwardRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import {translatableTextPropTypes} from '@libs/Localize'; import type {BottomTabNavigatorParamList, CentralPaneNavigatorParamList, SettingsNavigatorParamList} from '@navigation/types'; import policyMemberPropType from '@pages/policyMemberPropType'; import * as Policy from '@userActions/Policy'; @@ -57,7 +58,7 @@ const policyPropTypes = { * } * } */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), /** Whether or not the policy requires tags */ requiresTag: PropTypes.bool, diff --git a/src/stories/Form.stories.js b/src/stories/Form.stories.js index 6a4274c87eda..8a152d040a1f 100644 --- a/src/stories/Form.stories.js +++ b/src/stories/Form.stories.js @@ -68,7 +68,7 @@ function Template(args) { label="Street" inputID="street" containerStyles={[defaultStyles.mt4]} - hint="No PO box" + hint="common.noPO" /> = Record = Record; -type Errors = Record; +type Errors = Record; type AvatarType = typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 92b39fc3ac50..cb31afbf8f8f 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -684,7 +684,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); expect(transaction.pendingAction).toBeFalsy(); expect(transaction.errors).toBeTruthy(); - expect(_.values(transaction.errors)[0]).toBe('iou.error.genericCreateFailureMessage'); + expect(_.values(transaction.errors)[0]).toEqual(expect.arrayContaining(['iou.error.genericCreateFailureMessage', {isTranslated: false}])); resolve(); }, }); @@ -1629,7 +1629,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); const updatedAction = _.find(allActions, (reportAction) => !_.isEmpty(reportAction)); expect(updatedAction.actionName).toEqual('MODIFIEDEXPENSE'); - expect(_.values(updatedAction.errors)).toEqual(expect.arrayContaining(['iou.error.genericEditFailureMessage'])); + expect(_.values(updatedAction.errors)).toEqual(expect.arrayContaining([['iou.error.genericEditFailureMessage', {isTranslated: false}]])); resolve(); }, }); @@ -1843,7 +1843,7 @@ describe('actions/IOU', () => { callback: (allActions) => { Onyx.disconnect(connectionID); const erroredAction = _.find(_.values(allActions), (action) => !_.isEmpty(action.errors)); - expect(_.values(erroredAction.errors)).toEqual(expect.arrayContaining(['iou.error.other'])); + expect(_.values(erroredAction.errors)).toEqual(expect.arrayContaining([['iou.error.other', {isTranslated: false}]])); resolve(); }, });