Skip to content

Commit 8f595d7

Browse files
committed
Refactor error handling to use TranslationKeyError instead of TranslationPathError across multiple components and utilities. Update related types and functions for consistency.
1 parent 5d66635 commit 8f595d7

7 files changed

Lines changed: 119 additions & 24 deletions

File tree

src/components/DotIndicatorMessage.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import useLocalize from '@hooks/useLocalize';
77
import useStyleUtils from '@hooks/useStyleUtils';
88
import useTheme from '@hooks/useTheme';
99
import useThemeStyles from '@hooks/useThemeStyles';
10-
import {isReceiptError, isTranslationPathError} from '@libs/ErrorUtils';
10+
import {isReceiptError, isTranslationKeyError} from '@libs/ErrorUtils';
1111
import fileDownload from '@libs/fileDownload';
1212
import handleRetryPress from '@libs/ReceiptUploadRetryHandler';
13-
import type {TranslationPathError} from '@src/languages/types';
13+
import type {TranslationKeyError} from '@src/languages/types';
1414
import type {ReceiptError} from '@src/types/onyx/Transaction';
1515
import ConfirmModal from './ConfirmModal';
1616
import Icon from './Icon';
@@ -26,7 +26,7 @@ type DotIndicatorMessageProps = {
2626
* timestamp: 'message',
2727
* }
2828
*/
29-
messages: Record<string, string | ReceiptError | TranslationPathError | ReactElement | null>;
29+
messages: Record<string, string | ReceiptError | TranslationKeyError | ReactElement | null>;
3030

3131
/** The type of message, 'error' shows a red dot, 'success' shows a green dot */
3232
type: 'error' | 'success';
@@ -110,7 +110,7 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles, dismissErr
110110
key={index}
111111
style={[StyleUtils.getDotIndicatorTextStyles(isErrorMessage), textStyles]}
112112
>
113-
{isTranslationPathError(message) ? translate(message.translationPath) : message}
113+
{isTranslationKeyError(message) ? translate(message.translationKey) : message}
114114
</Text>
115115
);
116116
};

src/components/ErrorMessageRow.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import mapValues from 'lodash/mapValues';
22
import React from 'react';
33
import type {StyleProp, ViewStyle} from 'react-native';
4-
import type {TranslationPathError, TranslationPathErrors} from '@src/languages/types';
4+
import type {TranslationKeyError, TranslationKeyErrors} from '@src/languages/types';
55
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
66
import type {ReceiptError, ReceiptErrors} from '@src/types/onyx/Transaction';
77
import {isEmptyObject} from '@src/types/utils/EmptyObject';
88
import MessagesRow from './MessagesRow';
99

1010
type ErrorMessageRowProps = {
1111
/** The errors to display */
12-
errors?: OnyxCommon.Errors | ReceiptErrors | TranslationPathErrors | null;
12+
errors?: OnyxCommon.Errors | ReceiptErrors | TranslationKeyErrors | null;
1313

1414
/** Additional style object for the error row */
1515
errorRowStyles?: StyleProp<ViewStyle>;
@@ -27,7 +27,7 @@ type ErrorMessageRowProps = {
2727
function ErrorMessageRow({errors, errorRowStyles, onClose, canDismissError = true, dismissError}: ErrorMessageRowProps) {
2828
// Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages.
2929
const errorEntries = Object.entries(errors ?? {});
30-
const filteredErrorEntries = errorEntries.filter((errorEntry): errorEntry is [string, string | ReceiptError | TranslationPathError] => errorEntry[1] !== null);
30+
const filteredErrorEntries = errorEntries.filter((errorEntry): errorEntry is [string, string | ReceiptError | TranslationKeyError] => errorEntry[1] !== null);
3131
const errorMessages = mapValues(Object.fromEntries(filteredErrorEntries), (error) => error);
3232
const hasErrorMessages = !isEmptyObject(errorMessages);
3333

src/components/MessagesRow.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
77
import CONST from '@src/CONST';
88
import type {ReceiptError} from '@src/types/onyx/Transaction';
99
import {isEmptyObject} from '@src/types/utils/EmptyObject';
10-
import type { TranslationPathError } from '@src/languages/types';
10+
import type { TranslationKeyError } from '@src/languages/types';
1111
import DotIndicatorMessage from './DotIndicatorMessage';
1212
import Icon from './Icon';
1313
import * as Expensicons from './Icon/Expensicons';
@@ -16,7 +16,7 @@ import Tooltip from './Tooltip';
1616

1717
type MessagesRowProps = {
1818
/** The messages to display */
19-
messages: Record<string, string | ReceiptError | TranslationPathError>;
19+
messages: Record<string, string | ReceiptError | TranslationKeyError>;
2020

2121
/** The type of message, 'error' shows a red dot, 'success' shows a green dot */
2222
type: 'error' | 'success';

src/components/OfflineWithFeedback.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
1212
import type {ReceiptErrors} from '@src/types/onyx/Transaction';
1313
import type ChildrenProps from '@src/types/utils/ChildrenProps';
1414
import {isEmptyObject} from '@src/types/utils/EmptyObject';
15-
import type { TranslationPathErrors } from '@src/languages/types';
15+
import type { TranslationKeyErrors } from '@src/languages/types';
1616
import CustomStylesForChildrenProvider from './CustomStylesForChildrenProvider';
1717
import ErrorMessageRow from './ErrorMessageRow';
1818
import ImageSVG from './ImageSVG';
@@ -31,7 +31,7 @@ type OfflineWithFeedbackProps = ChildrenProps & {
3131
shouldHideOnDelete?: boolean;
3232

3333
/** The errors to display */
34-
errors?: OnyxCommon.Errors | ReceiptErrors | TranslationPathErrors | null;
34+
errors?: OnyxCommon.Errors | ReceiptErrors | TranslationKeyErrors | null;
3535

3636
/** Whether we should show the error messages */
3737
shouldShowErrorMessages?: boolean;

src/languages/types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ type DefaultTranslation = typeof en;
7676
*/
7777
type TranslationPaths = FlattenObject<DefaultTranslation>;
7878

79-
type TranslationPathError = {
80-
translationPath: TranslationPaths
79+
type TranslationKeyError = {
80+
translationKey: TranslationPaths
8181
}
8282

83-
type TranslationPathErrors = Record<string, TranslationPathError>;
83+
type TranslationKeyErrors = Record<string, TranslationKeyError>;
8484
/**
8585
* Flattened default translation object with its values
8686
*/
@@ -99,4 +99,4 @@ type TranslationParameters<TKey extends TranslationPaths> = FlatTranslationsObje
9999
: Args
100100
: never[];
101101

102-
export type {TranslationDeepObject, TranslationPaths, PluralForm, FlatTranslationsObject, TranslationParameters, TranslationPathError, TranslationPathErrors};
102+
export type {TranslationDeepObject, TranslationPaths, PluralForm, FlatTranslationsObject, TranslationParameters, TranslationKeyError, TranslationKeyErrors};

src/libs/ErrorUtils.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import mapValues from 'lodash/mapValues';
22
import type {OnyxEntry} from 'react-native-onyx';
33
import CONST from '@src/CONST';
4-
import type {TranslationPathError, TranslationPathErrors, TranslationPaths} from '@src/languages/types';
4+
import type {TranslationKeyError, TranslationKeyErrors, TranslationPaths} from '@src/languages/types';
55
import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon';
66
import type Response from '@src/types/onyx/Response';
77
import type {ReceiptError} from '@src/types/onyx/Transaction';
88
import DateUtils from './DateUtils';
9-
import * as Localize from './Localize';
9+
import {translateLocal} from './Localize';
1010

1111
function getAuthenticateErrorMessage(response: Response): TranslationPaths {
1212
switch (response.jsonCode) {
@@ -42,11 +42,15 @@ function getAuthenticateErrorMessage(response: Response): TranslationPaths {
4242
* @param error - The translation key for the error message.
4343
*/
4444
function getMicroSecondOnyxErrorWithTranslationKey(error: TranslationPaths, errorKey?: number): Errors {
45-
return {[errorKey ?? DateUtils.getMicroseconds()]: Localize.translateLocal(error)};
45+
return {[errorKey ?? DateUtils.getMicroseconds()]: translateLocal(error)};
4646
}
4747

48-
function getMicroSecondTranslationErrorWithTranslationKey(error: TranslationPaths, errorKey?: number): TranslationPathErrors {
49-
return {[errorKey ?? DateUtils.getMicroseconds()]: {translationPath: error}};
48+
/**
49+
* Creates an error object with a timestamp (in microseconds) as the key and the translation key as the value.
50+
* @param translationKey - The translation key for the error message.
51+
*/
52+
function getMicroSecondTranslationErrorWithTranslationKey(translationKey: TranslationPaths, errorKey?: number): TranslationKeyErrors {
53+
return {[errorKey ?? DateUtils.getMicroseconds()]: {translationKey}};
5054
}
5155

5256
/**
@@ -201,17 +205,20 @@ function isReceiptError(message: unknown): message is ReceiptError {
201205
return ((message as Record<string, unknown>)?.error ?? '') === CONST.IOU.RECEIPT_ERROR;
202206
}
203207

204-
function isTranslationPathError(message: unknown): message is TranslationPathError {
208+
/**
209+
* Check if the error includes a translation key.
210+
*/
211+
function isTranslationKeyError(message: unknown): message is TranslationKeyError {
205212
if (typeof message === 'string') {
206213
return false;
207214
}
208215
if (Array.isArray(message)) {
209216
return false;
210217
}
211-
if (Object.keys(message as Record<string, unknown>).length === 0) {
218+
if (Object.keys(message as Record<string, unknown>).length !== 1) {
212219
return false;
213220
}
214-
return (message as Record<string, unknown>)?.translationPath !== undefined;
221+
return (message as Record<string, unknown>)?.translationKey !== undefined;
215222
}
216223

217224
export {
@@ -229,7 +236,7 @@ export {
229236
getMicroSecondOnyxErrorWithMessage,
230237
getMicroSecondOnyxErrorObject,
231238
isReceiptError,
232-
isTranslationPathError,
239+
isTranslationKeyError,
233240
getMicroSecondTranslationErrorWithTranslationKey,
234241
};
235242

tests/unit/ErrorUtilsTest.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import * as ErrorUtils from '@src/libs/ErrorUtils';
2+
import DateUtils from '@src/libs/DateUtils';
23
import type {Errors} from '@src/types/onyx/OnyxCommon';
34

5+
// Mock DateUtils
6+
jest.mock('@src/libs/DateUtils');
7+
48
describe('ErrorUtils', () => {
59
test('should add a new error message for a given inputID', () => {
610
const errors: Errors = {};
@@ -62,4 +66,88 @@ describe('ErrorUtils', () => {
6266
username: 'Username cannot be empty\nUsername must be at least 6 characters long\nUsername must contain at least one letter\nUsername must not contain special characters',
6367
});
6468
});
69+
70+
describe('getMicroSecondTranslationErrorWithTranslationKey', () => {
71+
beforeEach(() => {
72+
jest.clearAllMocks();
73+
});
74+
75+
test('should create an error object with microsecond timestamp and translation key', () => {
76+
const mockMicroseconds = 1234567890123;
77+
(DateUtils.getMicroseconds as jest.Mock).mockReturnValue(mockMicroseconds);
78+
79+
const result = ErrorUtils.getMicroSecondTranslationErrorWithTranslationKey('passwordForm.error.incorrectLoginOrPassword');
80+
81+
expect(result).toEqual({
82+
[mockMicroseconds]: {translationKey: 'passwordForm.error.incorrectLoginOrPassword'},
83+
});
84+
expect(DateUtils.getMicroseconds).toHaveBeenCalledTimes(1);
85+
});
86+
87+
test('should use provided errorKey instead of generating microsecond timestamp', () => {
88+
const customErrorKey = 9876543210;
89+
const result = ErrorUtils.getMicroSecondTranslationErrorWithTranslationKey('passwordForm.error.fallback', customErrorKey);
90+
91+
expect(result).toEqual({
92+
[customErrorKey]: {translationKey: 'passwordForm.error.fallback'},
93+
});
94+
expect(DateUtils.getMicroseconds).not.toHaveBeenCalled();
95+
});
96+
97+
test('should handle different translation keys', () => {
98+
const mockMicroseconds = 1111111111111;
99+
(DateUtils.getMicroseconds as jest.Mock).mockReturnValue(mockMicroseconds);
100+
101+
const result = ErrorUtils.getMicroSecondTranslationErrorWithTranslationKey('passwordForm.error.incorrectLoginOrPassword');
102+
103+
expect(result).toEqual({
104+
[mockMicroseconds]: {translationKey: 'passwordForm.error.incorrectLoginOrPassword'},
105+
});
106+
});
107+
});
108+
109+
describe('isTranslationKeyError', () => {
110+
test('should return false for string messages', () => {
111+
expect(ErrorUtils.isTranslationKeyError('This is a string error')).toBe(false);
112+
expect(ErrorUtils.isTranslationKeyError('')).toBe(false);
113+
});
114+
115+
test('should return false for array messages', () => {
116+
expect(ErrorUtils.isTranslationKeyError(['error1', 'error2'])).toBe(false);
117+
expect(ErrorUtils.isTranslationKeyError([])).toBe(false);
118+
});
119+
120+
test('should return false for empty objects', () => {
121+
expect(ErrorUtils.isTranslationKeyError({})).toBe(false);
122+
});
123+
124+
test('should return false for objects with multiple keys', () => {
125+
expect(ErrorUtils.isTranslationKeyError({
126+
translationKey: 'passwordForm.error.fallback',
127+
extraKey: 'extra',
128+
})).toBe(false);
129+
});
130+
131+
test('should return false for objects without translationKey property', () => {
132+
expect(ErrorUtils.isTranslationKeyError({error: 'generic error'})).toBe(false);
133+
expect(ErrorUtils.isTranslationKeyError({message: 'error message'})).toBe(false);
134+
});
135+
136+
test('should return true for valid TranslationKeyError objects', () => {
137+
expect(ErrorUtils.isTranslationKeyError({translationKey: 'passwordForm.error.fallback'})).toBe(true);
138+
expect(ErrorUtils.isTranslationKeyError({translationKey: 'passwordForm.error.incorrectLoginOrPassword'})).toBe(true);
139+
expect(ErrorUtils.isTranslationKeyError({translationKey: 'session.offlineMessageRetry'})).toBe(true);
140+
});
141+
142+
test('should return false for null and undefined', () => {
143+
expect(ErrorUtils.isTranslationKeyError(null)).toBe(false);
144+
expect(ErrorUtils.isTranslationKeyError(undefined)).toBe(false);
145+
});
146+
147+
test('should return false for primitive types', () => {
148+
expect(ErrorUtils.isTranslationKeyError(123)).toBe(false);
149+
expect(ErrorUtils.isTranslationKeyError(true)).toBe(false);
150+
expect(ErrorUtils.isTranslationKeyError(false)).toBe(false);
151+
});
152+
});
65153
});

0 commit comments

Comments
 (0)