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();
},
});