From 60ea1a16422b9452a62464600f73d89da28b8beb Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 26 Feb 2024 16:03:36 +0500 Subject: [PATCH 01/21] perf: remove redundant translations --- src/CONST.ts | 1 + src/libs/PersonalDetailsUtils.ts | 9 +++++++-- src/libs/ReportUtils.ts | 23 +++++++++++++++-------- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 8abd4c087b16..bb167bb5d8d8 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -41,6 +41,7 @@ const KEYBOARD_SHORTCUT_NAVIGATION_TYPE = 'NAVIGATION_SHORTCUT'; const cardActiveStates: number[] = [2, 3, 4, 7]; const CONST = { + MERGED_ACCOUNT_PREFIX: 'MERGED_', ANDROID_PACKAGE_NAME, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 55aee10e611a..7cd6ac5bdedd 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -24,9 +24,14 @@ Onyx.connect({ }, }); +const hiddenText = Localize.translateLocal('common.hidden'); +const substringStartIndex = 8; function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true): string { - const displayName = passedPersonalDetails?.displayName ? passedPersonalDetails.displayName.replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '') : ''; - const fallbackValue = shouldFallbackToHidden ? Localize.translateLocal('common.hidden') : ''; + let displayName = passedPersonalDetails?.displayName ?? ''; + if (displayName.startsWith(CONST.MERGED_ACCOUNT_PREFIX)) { + displayName = displayName.substring(substringStartIndex); + } + const fallbackValue = shouldFallbackToHidden ? hiddenText : ''; return displayName || defaultValue || fallbackValue; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8813501e2b3f..adfefec932e5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -575,17 +575,18 @@ function getPolicyType(report: OnyxEntry, policies: OnyxCollection | undefined | EmptyObject, returnEmptyIfNotFound = false, policy: OnyxEntry | undefined = undefined): string { - const noPolicyFound = returnEmptyIfNotFound ? '' : Localize.translateLocal('workspace.common.unavailable'); + const noPolicyFound = returnEmptyIfNotFound ? '' : unavailableWorkspaceText; if (isEmptyObject(report)) { return noPolicyFound; } if ((!allPolicies || Object.keys(allPolicies).length === 0) && !report?.policyName) { - return Localize.translateLocal('workspace.common.unavailable'); + return unavailableWorkspaceText; } const finalPolicy = policy ?? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; @@ -773,11 +774,12 @@ function isAnnounceRoom(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE; } +const chatTypes = [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL]; /** * Whether the provided report is a default room */ function isDefaultRoom(report: OnyxEntry): boolean { - return [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL].some((type) => type === getChatType(report)); + return chatTypes.some((type) => type === getChatType(report)); } /** @@ -1625,6 +1627,7 @@ function getPersonalDetailsForAccountID(accountID: number): Partial, policy: OnyxEntry = nu } if (parentReportAction?.message?.[0]?.isDeletedParentAction) { - return Localize.translateLocal('parentReportAction.deletedMessage'); + return deletedMessageText; } const isAttachment = ReportActionsUtils.isReportActionAttachment(!isEmptyObject(parentReportAction) ? parentReportAction : null); const parentReportActionMessage = (parentReportAction?.message?.[0]?.text ?? '').replace(/(\r\n|\n|\r)/gm, ' '); if (isAttachment && parentReportActionMessage) { - return `[${Localize.translateLocal('common.attachment')}]`; + return `[${attachmentText}]`; } if ( parentReportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || @@ -2529,7 +2536,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu } if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) { - return Localize.translateLocal('parentReportAction.deletedTask'); + return deletedTaskText; } if (isChatRoom(report) || isTaskReport(report)) { @@ -2545,7 +2552,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu } if (isArchivedRoom(report)) { - formattedName += ` (${Localize.translateLocal('common.archived')})`; + formattedName += ` (${archivedText})`; } if (formattedName) { From 22f0ffa46f75a5828e6edbf6774c0b53e688fbda Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 26 Feb 2024 16:29:41 +0500 Subject: [PATCH 02/21] perf: only parse the number if it matches the pattern --- src/CONST.ts | 1 + src/libs/LocalePhoneNumber.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index bb167bb5d8d8..fce096a9957a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -42,6 +42,7 @@ const cardActiveStates: number[] = [2, 3, 4, 7]; const CONST = { MERGED_ACCOUNT_PREFIX: 'MERGED_', + SMS_DOMAIN_PATTERN: 'expensify.sms', ANDROID_PACKAGE_NAME, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, diff --git a/src/libs/LocalePhoneNumber.ts b/src/libs/LocalePhoneNumber.ts index 933aa7937560..4fc13322b0cf 100644 --- a/src/libs/LocalePhoneNumber.ts +++ b/src/libs/LocalePhoneNumber.ts @@ -1,5 +1,6 @@ import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {parsePhoneNumber} from './PhoneNumber'; @@ -18,6 +19,10 @@ function formatPhoneNumber(number: string): string { return ''; } + // do not parse the string, if it's not a phone number + if (number.indexOf(CONST.SMS_DOMAIN_PATTERN) === -1) { + return number; + } const numberWithoutSMSDomain = Str.removeSMSDomain(number); const parsedPhoneNumber = parsePhoneNumber(numberWithoutSMSDomain); From 643c7e150cd9e7f42f559c5df0e384182c7a0c0d Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 27 Feb 2024 12:44:01 +0500 Subject: [PATCH 03/21] perf: return early if displayName exists --- src/libs/PersonalDetailsUtils.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 7cd6ac5bdedd..912e9aba36aa 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -25,14 +25,22 @@ Onyx.connect({ }); const hiddenText = Localize.translateLocal('common.hidden'); -const substringStartIndex = 8; +const substringStartIndex = CONST.MERGED_ACCOUNT_PREFIX.length; function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true): string { let displayName = passedPersonalDetails?.displayName ?? ''; if (displayName.startsWith(CONST.MERGED_ACCOUNT_PREFIX)) { displayName = displayName.substring(substringStartIndex); } + + /** + * If displayName exists, return it early so we don't have to allocate + * memory for the fallback string. + */ + if (displayName) { + return displayName; + } const fallbackValue = shouldFallbackToHidden ? hiddenText : ''; - return displayName || defaultValue || fallbackValue; + return defaultValue || fallbackValue; } /** From c683db7392b2ad06320785a78661bf56c5c2bc4e Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 27 Feb 2024 13:10:35 +0500 Subject: [PATCH 04/21] test: fix failing test for formatPhoneNumber --- src/libs/LocalePhoneNumber.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/libs/LocalePhoneNumber.ts b/src/libs/LocalePhoneNumber.ts index 4fc13322b0cf..ec704ec4d44b 100644 --- a/src/libs/LocalePhoneNumber.ts +++ b/src/libs/LocalePhoneNumber.ts @@ -10,6 +10,28 @@ Onyx.connect({ callback: (val) => (countryCodeByIP = val ?? 1), }); +/** + * Checks whether the given string contains any numbers. + * It uses indexOf instead of regex and includes for performance reasons. + * + * @param text + * @returns boolean + */ +function containsNumbers(text: string) { + return ( + text.indexOf('0') !== -1 || + text.indexOf('1') !== -1 || + text.indexOf('2') !== -1 || + text.indexOf('3') !== -1 || + text.indexOf('4') !== -1 || + text.indexOf('5') !== -1 || + text.indexOf('6') !== -1 || + text.indexOf('7') !== -1 || + text.indexOf('8') !== -1 || + text.indexOf('9') !== -1 + ); +} + /** * Returns a locally converted phone number for numbers from the same region * and an internationally converted phone number with the country code for numbers from other regions @@ -20,7 +42,7 @@ function formatPhoneNumber(number: string): string { } // do not parse the string, if it's not a phone number - if (number.indexOf(CONST.SMS_DOMAIN_PATTERN) === -1) { + if (number.indexOf(CONST.SMS_DOMAIN_PATTERN) === -1 && !containsNumbers(number)) { return number; } const numberWithoutSMSDomain = Str.removeSMSDomain(number); From 454a9b258fee05e310d2378b5bfd2cdaf915b251 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 27 Feb 2024 15:24:10 +0500 Subject: [PATCH 05/21] refactor: move translations to utils --- src/libs/CommonTranslationUtils.ts | 49 ++++++++++++++++++++++++++++++ src/libs/PersonalDetailsUtils.ts | 11 +++++-- src/libs/ReportUtils.ts | 21 +++++-------- 3 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 src/libs/CommonTranslationUtils.ts diff --git a/src/libs/CommonTranslationUtils.ts b/src/libs/CommonTranslationUtils.ts new file mode 100644 index 000000000000..80a06e51ce9f --- /dev/null +++ b/src/libs/CommonTranslationUtils.ts @@ -0,0 +1,49 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import * as Localize from './Localize'; + +/** + * This file contains common translations that are used in multiple places in the app. + * This is done to avoid duplicate translations and to keep the translations consistent. + * This also allows us to not repeatedly translate the same string which may happen due + * to translations being done for eg, in a loop. + * + * This was identified as part of a performance audit. + * details: https://github.com/Expensify/App/issues/35234#issuecomment-1926911643 + */ + +let deletedTaskText = ''; +let deletedMessageText = ''; +let attachmentText = ''; +let archivedText = ''; +let hiddenText = ''; +let unavailableWorkspaceText = ''; + +function isTranslationAvailable() { + return deletedTaskText && deletedMessageText && attachmentText && archivedText && hiddenText && unavailableWorkspaceText; +} + +Onyx.connect({ + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + callback: (val) => { + if (!val && isTranslationAvailable()) { + return; + } + + deletedTaskText = Localize.translateLocal('parentReportAction.deletedTask'); + deletedMessageText = Localize.translateLocal('parentReportAction.deletedMessage'); + attachmentText = Localize.translateLocal('common.attachment'); + archivedText = Localize.translateLocal('common.archived'); + hiddenText = Localize.translateLocal('common.hidden'); + unavailableWorkspaceText = Localize.translateLocal('workspace.common.unavailable'); + }, +}); + +export default { + deletedTaskText: () => deletedTaskText, + deletedMessageText: () => deletedMessageText, + attachmentText: () => attachmentText, + archivedText: () => archivedText, + hiddenText: () => hiddenText, + unavailableWorkspaceText: () => unavailableWorkspaceText, +}; diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 912e9aba36aa..7a9b3fe96598 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -5,6 +5,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, PersonalDetailsList, PrivatePersonalDetails} from '@src/types/onyx'; import type {OnyxData} from '@src/types/onyx/Request'; +import CommonTranslationUtils from './CommonTranslationUtils'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; import * as UserUtils from './UserUtils'; @@ -24,10 +25,16 @@ Onyx.connect({ }, }); -const hiddenText = Localize.translateLocal('common.hidden'); +/** + * Index for the substring method to remove the merged account prefix. + */ const substringStartIndex = CONST.MERGED_ACCOUNT_PREFIX.length; + function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true): string { let displayName = passedPersonalDetails?.displayName ?? ''; + /** + * If the displayName starts with the merged account prefix, remove it. + */ if (displayName.startsWith(CONST.MERGED_ACCOUNT_PREFIX)) { displayName = displayName.substring(substringStartIndex); } @@ -39,7 +46,7 @@ function getDisplayNameOrDefault(passedPersonalDetails?: Partial, policies: OnyxCollection | undefined | EmptyObject, returnEmptyIfNotFound = false, policy: OnyxEntry | undefined = undefined): string { - const noPolicyFound = returnEmptyIfNotFound ? '' : unavailableWorkspaceText; + const noPolicyFound = returnEmptyIfNotFound ? '' : CommonTranslationUtils.unavailableWorkspaceText(); if (isEmptyObject(report)) { return noPolicyFound; } if ((!allPolicies || Object.keys(allPolicies).length === 0) && !report?.policyName) { - return unavailableWorkspaceText; + return CommonTranslationUtils.unavailableWorkspaceText(); } const finalPolicy = policy ?? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; @@ -1627,7 +1627,6 @@ function getPersonalDetailsForAccountID(accountID: number): Partial, policy: OnyxEntry = nu } if (parentReportAction?.message?.[0]?.isDeletedParentAction) { - return deletedMessageText; + return CommonTranslationUtils.deletedMessageText(); } const isAttachment = ReportActionsUtils.isReportActionAttachment(!isEmptyObject(parentReportAction) ? parentReportAction : null); const parentReportActionMessage = (parentReportAction?.message?.[0]?.text ?? '').replace(/(\r\n|\n|\r)/gm, ' '); if (isAttachment && parentReportActionMessage) { - return `[${attachmentText}]`; + return `[${CommonTranslationUtils.attachmentText()}]`; } if ( parentReportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || @@ -2536,7 +2531,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu } if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) { - return deletedTaskText; + return CommonTranslationUtils.deletedTaskText(); } if (isChatRoom(report) || isTaskReport(report)) { @@ -2552,7 +2547,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu } if (isArchivedRoom(report)) { - formattedName += ` (${archivedText})`; + formattedName += ` (${CommonTranslationUtils.archivedText()})`; } if (formattedName) { From eb8f26ccb2fabb54fbb7f6c054e13dd53127c9ee Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 27 Feb 2024 15:24:28 +0500 Subject: [PATCH 06/21] fix: linting --- src/libs/LocalePhoneNumber.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/LocalePhoneNumber.ts b/src/libs/LocalePhoneNumber.ts index ec704ec4d44b..798e0a50c43e 100644 --- a/src/libs/LocalePhoneNumber.ts +++ b/src/libs/LocalePhoneNumber.ts @@ -13,7 +13,7 @@ Onyx.connect({ /** * Checks whether the given string contains any numbers. * It uses indexOf instead of regex and includes for performance reasons. - * + * * @param text * @returns boolean */ From 34cd2e8402fe6fab0135e503114b7d514360a76b Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 28 Feb 2024 13:03:14 +0500 Subject: [PATCH 07/21] refactor: use regex and dedicated const for sms domain --- src/CONST.ts | 1 - src/libs/LocalePhoneNumber.ts | 26 ++------------------------ 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index fce096a9957a..bb167bb5d8d8 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -42,7 +42,6 @@ const cardActiveStates: number[] = [2, 3, 4, 7]; const CONST = { MERGED_ACCOUNT_PREFIX: 'MERGED_', - SMS_DOMAIN_PATTERN: 'expensify.sms', ANDROID_PACKAGE_NAME, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, diff --git a/src/libs/LocalePhoneNumber.ts b/src/libs/LocalePhoneNumber.ts index 798e0a50c43e..460d5fc0fe9f 100644 --- a/src/libs/LocalePhoneNumber.ts +++ b/src/libs/LocalePhoneNumber.ts @@ -10,28 +10,6 @@ Onyx.connect({ callback: (val) => (countryCodeByIP = val ?? 1), }); -/** - * Checks whether the given string contains any numbers. - * It uses indexOf instead of regex and includes for performance reasons. - * - * @param text - * @returns boolean - */ -function containsNumbers(text: string) { - return ( - text.indexOf('0') !== -1 || - text.indexOf('1') !== -1 || - text.indexOf('2') !== -1 || - text.indexOf('3') !== -1 || - text.indexOf('4') !== -1 || - text.indexOf('5') !== -1 || - text.indexOf('6') !== -1 || - text.indexOf('7') !== -1 || - text.indexOf('8') !== -1 || - text.indexOf('9') !== -1 - ); -} - /** * Returns a locally converted phone number for numbers from the same region * and an internationally converted phone number with the country code for numbers from other regions @@ -41,8 +19,8 @@ function formatPhoneNumber(number: string): string { return ''; } - // do not parse the string, if it's not a phone number - if (number.indexOf(CONST.SMS_DOMAIN_PATTERN) === -1 && !containsNumbers(number)) { + // do not parse the string, if it doesn't contain the SMS domain and it's not a phone number + if (number.indexOf(CONST.SMS.DOMAIN) === -1 && !CONST.REGEX.DIGITS_AND_PLUS.test(number)) { return number; } const numberWithoutSMSDomain = Str.removeSMSDomain(number); From 707f56c7f913dcf6e7e764933c7e3b7f78021294 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 28 Feb 2024 13:03:42 +0500 Subject: [PATCH 08/21] refactor: remove allocating a variable and return the evaluated expresssion --- src/libs/PersonalDetailsUtils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 7a9b3fe96598..d5946feb2f74 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -46,8 +46,7 @@ function getDisplayNameOrDefault(passedPersonalDetails?: Partial Date: Thu, 29 Feb 2024 13:00:44 +0500 Subject: [PATCH 09/21] refactor: use single-line comments --- src/libs/PersonalDetailsUtils.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index d5946feb2f74..97e2dc91492b 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -25,24 +25,19 @@ Onyx.connect({ }, }); -/** - * Index for the substring method to remove the merged account prefix. - */ +// Index for the substring method to remove the merged account prefix. const substringStartIndex = CONST.MERGED_ACCOUNT_PREFIX.length; function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true): string { let displayName = passedPersonalDetails?.displayName ?? ''; - /** - * If the displayName starts with the merged account prefix, remove it. - */ + + // If the displayName starts with the merged account prefix, remove it. if (displayName.startsWith(CONST.MERGED_ACCOUNT_PREFIX)) { displayName = displayName.substring(substringStartIndex); } - /** - * If displayName exists, return it early so we don't have to allocate - * memory for the fallback string. - */ + // If displayName exists, return it early so we don't have to allocate + // memory for the fallback string. if (displayName) { return displayName; } From e02b55a5f95fe10e4f22a28b16f20aaf75977c5f Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 4 Mar 2024 13:02:21 +0500 Subject: [PATCH 10/21] refactor: use default policy room chat types from CONST --- src/CONST.ts | 17 ++++++++++------- src/libs/ReportUtils.ts | 3 +-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index bb167bb5d8d8..78c562ea2cde 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -37,11 +37,20 @@ const keyInputRightArrow = KeyCommand?.constants?.keyInputRightArrow ?? 'keyInpu // describes if a shortcut key can cause navigation const KEYBOARD_SHORTCUT_NAVIGATION_TYPE = 'NAVIGATION_SHORTCUT'; +const chatTypes = { + POLICY_ANNOUNCE: 'policyAnnounce', + POLICY_ADMINS: 'policyAdmins', + DOMAIN_ALL: 'domainAll', + POLICY_ROOM: 'policyRoom', + POLICY_EXPENSE_CHAT: 'policyExpenseChat', +} as const; + // Explicit type annotation is required const cardActiveStates: number[] = [2, 3, 4, 7]; const CONST = { MERGED_ACCOUNT_PREFIX: 'MERGED_', + DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], ANDROID_PACKAGE_NAME, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, @@ -684,13 +693,7 @@ const CONST = { IOU: 'iou', TASK: 'task', }, - CHAT_TYPE: { - POLICY_ANNOUNCE: 'policyAnnounce', - POLICY_ADMINS: 'policyAdmins', - DOMAIN_ALL: 'domainAll', - POLICY_ROOM: 'policyRoom', - POLICY_EXPENSE_CHAT: 'policyExpenseChat', - }, + CHAT_TYPE: chatTypes, WORKSPACE_CHAT_ROOMS: { ANNOUNCE: '#announce', ADMINS: '#admins', diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 37fee63780fb..93d2fd5da37d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -774,12 +774,11 @@ function isAnnounceRoom(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE; } -const chatTypes = [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL]; /** * Whether the provided report is a default room */ function isDefaultRoom(report: OnyxEntry): boolean { - return chatTypes.some((type) => type === getChatType(report)); + return CONST.DEFAULT_POLICY_ROOM_CHAT_TYPES.some((type) => type === getChatType(report)); } /** From 7cbca08a4bd4b93ec49f6277df41f476744a33e7 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 4 Mar 2024 15:55:46 +0500 Subject: [PATCH 11/21] revert: bring back localize.translateLocal --- src/libs/PersonalDetailsUtils.ts | 2 +- src/libs/ReportUtils.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 97e2dc91492b..5a3de690d8f8 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -41,7 +41,7 @@ function getDisplayNameOrDefault(passedPersonalDetails?: Partial, policies: OnyxCollection | undefined | EmptyObject, returnEmptyIfNotFound = false, policy: OnyxEntry | undefined = undefined): string { - const noPolicyFound = returnEmptyIfNotFound ? '' : CommonTranslationUtils.unavailableWorkspaceText(); + const noPolicyFound = returnEmptyIfNotFound ? '' : Localize.translateLocal('workspace.common.unavailable'); if (isEmptyObject(report)) { return noPolicyFound; } if ((!allPolicies || Object.keys(allPolicies).length === 0) && !report?.policyName) { - return CommonTranslationUtils.unavailableWorkspaceText(); + return Localize.translateLocal('workspace.common.unavailable'); } const finalPolicy = policy ?? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; @@ -1647,7 +1647,7 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f const longName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, formattedLogin, shouldFallbackToHidden); // If the user's personal details (first name) should be hidden, make sure we return "hidden" instead of the short name - if (shouldFallbackToHidden && longName === CommonTranslationUtils.hiddenText()) { + if (shouldFallbackToHidden && longName === Localize.translateLocal('common.hidden')) { return longName; } @@ -2508,13 +2508,13 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu } if (parentReportAction?.message?.[0]?.isDeletedParentAction) { - return CommonTranslationUtils.deletedMessageText(); + return Localize.translateLocal('parentReportAction.deletedMessage'); } const isAttachment = ReportActionsUtils.isReportActionAttachment(!isEmptyObject(parentReportAction) ? parentReportAction : null); const parentReportActionMessage = (parentReportAction?.message?.[0]?.text ?? '').replace(/(\r\n|\n|\r)/gm, ' '); if (isAttachment && parentReportActionMessage) { - return `[${CommonTranslationUtils.attachmentText()}]`; + return `[${Localize.translateLocal('common.attachment')}]`; } if ( parentReportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || @@ -2530,7 +2530,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu } if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) { - return CommonTranslationUtils.deletedTaskText(); + return Localize.translateLocal('parentReportAction.deletedTask'); } if (isChatRoom(report) || isTaskReport(report)) { @@ -2546,7 +2546,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu } if (isArchivedRoom(report)) { - formattedName += ` (${CommonTranslationUtils.archivedText()})`; + formattedName += ` (${Localize.translateLocal('common.archived')})`; } if (formattedName) { From 4df1d3da5dd9f6cb7862c8c62b105688cbbe9274 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 4 Mar 2024 16:51:30 +0500 Subject: [PATCH 12/21] feat: add cache for translated values in Localize --- src/libs/Localize/index.ts | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 64d07897aa8a..9cadc352ca9a 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -93,11 +93,42 @@ function translate(desiredLanguage: 'en' | 'es' | throw new Error(`${phraseKey} was not found in the default language`); } +/** + * Map to store translated values for each locale + * This is used to avoid translating the same phrase multiple times. + * + * The data is stored in the following format: + * + * { + * "name_en": "Name", + * "name_es": "Nombre", + * } + * + * Note: We are not storing any translated values for phrases with variables, + * as they have higher chance of being unique, so we'll end up wasting space + * in our cache. + */ +const translatedValues = new Map(); + /** * Uses the locale in this file updated by the Onyx subscriber. */ function translateLocal(phrase: TKey, ...variables: PhraseParameters>) { - return translate(BaseLocaleListener.getPreferredLocale(), phrase, ...variables); + const preferredLocale = BaseLocaleListener.getPreferredLocale(); + const key = `${phrase}_${preferredLocale}`; + const isVariablesEmpty = variables.length === 0; + + // If the phrase is already translated and there are no variables, return the translated value + if (translatedValues.has(key) && isVariablesEmpty) { + return translatedValues.get(key) as string; + } + const translatedText = translate(preferredLocale, phrase, ...variables); + + // We don't want to store translated values for phrases with variables + if (isVariablesEmpty) { + translatedValues.set(key, translatedText); + } + return translatedText; } /** From 55eb5d2f765099561de1a8720e2cb1345ca013e3 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 4 Mar 2024 17:13:32 +0500 Subject: [PATCH 13/21] refactor: delete common translation utils --- src/libs/CommonTranslationUtils.ts | 49 ------------------------------ 1 file changed, 49 deletions(-) delete mode 100644 src/libs/CommonTranslationUtils.ts diff --git a/src/libs/CommonTranslationUtils.ts b/src/libs/CommonTranslationUtils.ts deleted file mode 100644 index 80a06e51ce9f..000000000000 --- a/src/libs/CommonTranslationUtils.ts +++ /dev/null @@ -1,49 +0,0 @@ -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import * as Localize from './Localize'; - -/** - * This file contains common translations that are used in multiple places in the app. - * This is done to avoid duplicate translations and to keep the translations consistent. - * This also allows us to not repeatedly translate the same string which may happen due - * to translations being done for eg, in a loop. - * - * This was identified as part of a performance audit. - * details: https://github.com/Expensify/App/issues/35234#issuecomment-1926911643 - */ - -let deletedTaskText = ''; -let deletedMessageText = ''; -let attachmentText = ''; -let archivedText = ''; -let hiddenText = ''; -let unavailableWorkspaceText = ''; - -function isTranslationAvailable() { - return deletedTaskText && deletedMessageText && attachmentText && archivedText && hiddenText && unavailableWorkspaceText; -} - -Onyx.connect({ - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - callback: (val) => { - if (!val && isTranslationAvailable()) { - return; - } - - deletedTaskText = Localize.translateLocal('parentReportAction.deletedTask'); - deletedMessageText = Localize.translateLocal('parentReportAction.deletedMessage'); - attachmentText = Localize.translateLocal('common.attachment'); - archivedText = Localize.translateLocal('common.archived'); - hiddenText = Localize.translateLocal('common.hidden'); - unavailableWorkspaceText = Localize.translateLocal('workspace.common.unavailable'); - }, -}); - -export default { - deletedTaskText: () => deletedTaskText, - deletedMessageText: () => deletedMessageText, - attachmentText: () => attachmentText, - archivedText: () => archivedText, - hiddenText: () => hiddenText, - unavailableWorkspaceText: () => unavailableWorkspaceText, -}; From e643960c7ef54a8e2451f052c926ab664be127a4 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 4 Mar 2024 17:43:24 +0500 Subject: [PATCH 14/21] refactor: avoid multiple cache lookups --- src/libs/Localize/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 9cadc352ca9a..ad5e93af24ee 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -118,9 +118,13 @@ function translateLocal(phrase: TKey, ...variable const key = `${phrase}_${preferredLocale}`; const isVariablesEmpty = variables.length === 0; - // If the phrase is already translated and there are no variables, return the translated value - if (translatedValues.has(key) && isVariablesEmpty) { - return translatedValues.get(key) as string; + // Directly access and assign the translated value from the cache, instead of + // going through map.has() and map.get() to avoid multiple lookups. + const valueFromCache = translatedValues.get(key); + + // If the phrase is already translated, return the translated value + if (valueFromCache) { + return valueFromCache; } const translatedText = translate(preferredLocale, phrase, ...variables); From f3d6f1f73255fe186eea218f6600365dbfe06e41 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 4 Mar 2024 17:43:39 +0500 Subject: [PATCH 15/21] fix: lint --- src/libs/PersonalDetailsUtils.ts | 1 - src/libs/ReportUtils.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index c5de7456629d..391cabef0790 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -5,7 +5,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, PersonalDetailsList, PrivatePersonalDetails} from '@src/types/onyx'; import type {OnyxData} from '@src/types/onyx/Request'; -import CommonTranslationUtils from './CommonTranslationUtils'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; import * as UserUtils from './UserUtils'; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1c64deff881e..f7d49389e200 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -51,7 +51,6 @@ import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; import * as CollectionUtils from './CollectionUtils'; -import CommonTranslationUtils from './CommonTranslationUtils'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import isReportMessageAttachment from './isReportMessageAttachment'; From 175556071115a7732be109d64241adf732851cf7 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 6 Mar 2024 14:20:49 +0500 Subject: [PATCH 16/21] refactor: use dedicated map for each locale --- src/libs/Localize/index.ts | 39 +++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index ad5e93af24ee..f64d39d5e24a 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -12,6 +12,7 @@ import type {Locale} from '@src/types/onyx'; import type {ReceiptError} from '@src/types/onyx/Transaction'; import LocaleListener from './LocaleListener'; import BaseLocaleListener from './LocaleListener/BaseLocaleListener'; +import type BaseLocale from './LocaleListener/types'; // Current user mail is needed for handling missing translations let userEmail = ''; @@ -94,33 +95,55 @@ function translate(desiredLanguage: 'en' | 'es' | } /** - * Map to store translated values for each locale + * Map to store translated values for each locale. * This is used to avoid translating the same phrase multiple times. * * The data is stored in the following format: * * { - * "name_en": "Name", - * "name_es": "Nombre", + * "name": "Name", * } * * Note: We are not storing any translated values for phrases with variables, * as they have higher chance of being unique, so we'll end up wasting space * in our cache. */ -const translatedValues = new Map(); +const TRANSLATED_VALUES_EN = new Map(); +const TRANSLATED_VALUES_ES = new Map(); +const TRANSLATED_VALUES_ES_ES = new Map(); +const TRANSLATED_VALUES_ES_ONFIDO = new Map(); + +/** + * Returns the map for the given locale. + */ +function getTranslatedValuesMap(locale: BaseLocale) { + switch (locale) { + case CONST.LOCALES.ES_ES: + return TRANSLATED_VALUES_ES_ES; + case CONST.LOCALES.ES_ES_ONFIDO: + return TRANSLATED_VALUES_ES_ONFIDO; + case CONST.LOCALES.ES: + return TRANSLATED_VALUES_ES; + case CONST.LOCALES.DEFAULT: + default: + return TRANSLATED_VALUES_EN; + } +} /** * Uses the locale in this file updated by the Onyx subscriber. */ function translateLocal(phrase: TKey, ...variables: PhraseParameters>) { const preferredLocale = BaseLocaleListener.getPreferredLocale(); - const key = `${phrase}_${preferredLocale}`; + const key = `${phrase}`; const isVariablesEmpty = variables.length === 0; + // Get the map for the preferred locale + const map = getTranslatedValuesMap(preferredLocale); + // Directly access and assign the translated value from the cache, instead of // going through map.has() and map.get() to avoid multiple lookups. - const valueFromCache = translatedValues.get(key); + const valueFromCache = map.get(key); // If the phrase is already translated, return the translated value if (valueFromCache) { @@ -130,7 +153,9 @@ function translateLocal(phrase: TKey, ...variable // We don't want to store translated values for phrases with variables if (isVariablesEmpty) { - translatedValues.set(key, translatedText); + // We set the translated value in the cache in the next iteration + // of the event loop to make this operation asynchronous. + setImmediate(() => map.set(key, translatedText)); } return translatedText; } From 99dd34cfdf973681a713100ea345ced47911e206 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 6 Mar 2024 14:32:23 +0500 Subject: [PATCH 17/21] refactor: remove setImmediate --- src/libs/Localize/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index f64d39d5e24a..72bb4dd89bb1 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -153,9 +153,8 @@ function translateLocal(phrase: TKey, ...variable // We don't want to store translated values for phrases with variables if (isVariablesEmpty) { - // We set the translated value in the cache in the next iteration - // of the event loop to make this operation asynchronous. - setImmediate(() => map.set(key, translatedText)); + // We set the translated value in the cache + map.set(key, translatedText); } return translatedText; } From 3ff2f9c8cdf8ab6a09bbbeaa51f99aed27d205ab Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 11 Mar 2024 13:24:42 +0500 Subject: [PATCH 18/21] refactor: remove not needed variable --- src/libs/PersonalDetailsUtils.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 391cabef0790..fc87d0843734 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -24,23 +24,19 @@ Onyx.connect({ }, }); -// Index for the substring method to remove the merged account prefix. -const substringStartIndex = CONST.MERGED_ACCOUNT_PREFIX.length; - function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true, shouldAddCurrentUserPostfix = false): string { let displayName = passedPersonalDetails?.displayName ?? ''; // If the displayName starts with the merged account prefix, remove it. if (displayName.startsWith(CONST.MERGED_ACCOUNT_PREFIX)) { - displayName = displayName.substring(substringStartIndex); + // Remove the merged account prefix from the displayName. + displayName = displayName.substring(CONST.MERGED_ACCOUNT_PREFIX.length); } if (shouldAddCurrentUserPostfix && !!displayName) { displayName = `${displayName} (${Localize.translateLocal('common.you').toLowerCase()})`; } - // If displayName exists, return it early so we don't have to allocate - // memory for the fallback string. if (displayName) { return displayName; } From 70bd88fb764f7cd3f1c1853590b73520b423f868 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 11 Mar 2024 14:12:46 +0500 Subject: [PATCH 19/21] refactor: use Map of Maps to hold the locales with translated phrases --- src/libs/Localize/index.ts | 41 ++++++++++++-------------------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 72bb4dd89bb1..d5af7c795527 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import * as RNLocalize from 'react-native-localize'; import Onyx from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import Log from '@libs/Log'; import type {MessageElementBase, MessageTextElement} from '@libs/MessageElement'; import Config from '@src/CONFIG'; @@ -12,7 +13,6 @@ import type {Locale} from '@src/types/onyx'; import type {ReceiptError} from '@src/types/onyx/Transaction'; import LocaleListener from './LocaleListener'; import BaseLocaleListener from './LocaleListener/BaseLocaleListener'; -import type BaseLocale from './LocaleListener/types'; // Current user mail is needed for handling missing translations let userEmail = ''; @@ -101,49 +101,34 @@ function translate(desiredLanguage: 'en' | 'es' | * The data is stored in the following format: * * { - * "name": "Name", + * "en": { + * "name": "Name", * } * * Note: We are not storing any translated values for phrases with variables, * as they have higher chance of being unique, so we'll end up wasting space * in our cache. */ -const TRANSLATED_VALUES_EN = new Map(); -const TRANSLATED_VALUES_ES = new Map(); -const TRANSLATED_VALUES_ES_ES = new Map(); -const TRANSLATED_VALUES_ES_ONFIDO = new Map(); - -/** - * Returns the map for the given locale. - */ -function getTranslatedValuesMap(locale: BaseLocale) { - switch (locale) { - case CONST.LOCALES.ES_ES: - return TRANSLATED_VALUES_ES_ES; - case CONST.LOCALES.ES_ES_ONFIDO: - return TRANSLATED_VALUES_ES_ONFIDO; - case CONST.LOCALES.ES: - return TRANSLATED_VALUES_ES; - case CONST.LOCALES.DEFAULT: - default: - return TRANSLATED_VALUES_EN; - } -} +const translationCache = new Map, Map>( + Object.values(CONST.LOCALES).reduce((cache, locale) => { + cache.push([locale, new Map()]); + return cache; + }, [] as Array<[ValueOf, Map]>), +); /** * Uses the locale in this file updated by the Onyx subscriber. */ function translateLocal(phrase: TKey, ...variables: PhraseParameters>) { const preferredLocale = BaseLocaleListener.getPreferredLocale(); - const key = `${phrase}`; const isVariablesEmpty = variables.length === 0; - // Get the map for the preferred locale - const map = getTranslatedValuesMap(preferredLocale); + // Get the cache for the preferred locale + const cacheForLocale = translationCache.get(preferredLocale); // Directly access and assign the translated value from the cache, instead of // going through map.has() and map.get() to avoid multiple lookups. - const valueFromCache = map.get(key); + const valueFromCache = cacheForLocale?.get(phrase); // If the phrase is already translated, return the translated value if (valueFromCache) { @@ -154,7 +139,7 @@ function translateLocal(phrase: TKey, ...variable // We don't want to store translated values for phrases with variables if (isVariablesEmpty) { // We set the translated value in the cache - map.set(key, translatedText); + cacheForLocale?.set(phrase, translatedText); } return translatedText; } From 9fc04dbb780d54df5849761ac8fcfbcf72c0db99 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 12 Mar 2024 14:20:10 +0500 Subject: [PATCH 20/21] refactor: move cache mechanism to translate function --- src/libs/Localize/index.ts | 98 ++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index d5af7c795527..27451e5296a3 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -50,6 +50,28 @@ function init() { type PhraseParameters = T extends (...args: infer A) => string ? A : never[]; type Phrase = TranslationFlatObject[TKey] extends (...args: infer A) => unknown ? (...args: A) => string : string; +/** + * Map to store translated values for each locale. + * This is used to avoid translating the same phrase multiple times. + * + * The data is stored in the following format: + * + * { + * "en": { + * "name": "Name", + * } + * + * Note: We are not storing any translated values for phrases with variables, + * as they have higher chance of being unique, so we'll end up wasting space + * in our cache. + */ +const translationCache = new Map, Map>( + Object.values(CONST.LOCALES).reduce((cache, locale) => { + cache.push([locale, new Map()]); + return cache; + }, [] as Array<[ValueOf, Map]>), +); + /** * Return translated string for given locale and phrase * @@ -59,16 +81,43 @@ type Phrase = TranslationFlatObject[TKey] extends function translate(desiredLanguage: 'en' | 'es' | 'es-ES' | 'es_ES', phraseKey: TKey, ...phraseParameters: PhraseParameters>): string { // Search phrase in full locale e.g. es-ES const language = desiredLanguage === CONST.LOCALES.ES_ES_ONFIDO ? CONST.LOCALES.ES_ES : desiredLanguage; + + // Get the cache for the above locale + let cacheForLocale = translationCache.get(language); + + // Directly access and assign the translated value from the cache, instead of + // going through map.has() and map.get() to avoid multiple lookups. + const valueFromCache = cacheForLocale?.get(phraseKey); + + // If the phrase is already translated, return the translated value + if (valueFromCache) { + return valueFromCache; + } + let translatedPhrase = translations?.[language]?.[phraseKey] as Phrase; if (translatedPhrase) { - return typeof translatedPhrase === 'function' ? translatedPhrase(...phraseParameters) : translatedPhrase; + if (typeof translatedPhrase === 'function') { + return translatedPhrase(...phraseParameters); + } + + // We set the translated value in the cache only for the phrases without parameters. + cacheForLocale?.set(phraseKey, translatedPhrase); + return translatedPhrase; } // Phrase is not found in full locale, search it in fallback language e.g. es const languageAbbreviation = desiredLanguage.substring(0, 2) as 'en' | 'es'; + // Get the cache for the above locale + cacheForLocale = translationCache.get(languageAbbreviation); translatedPhrase = translations?.[languageAbbreviation]?.[phraseKey] as Phrase; if (translatedPhrase) { - return typeof translatedPhrase === 'function' ? translatedPhrase(...phraseParameters) : translatedPhrase; + if (typeof translatedPhrase === 'function') { + return translatedPhrase(...phraseParameters); + } + + // We set the translated value in the cache only for the phrases without parameters. + cacheForLocale?.set(phraseKey, translatedPhrase); + return translatedPhrase; } if (languageAbbreviation !== CONST.LOCALES.DEFAULT) { @@ -94,54 +143,11 @@ function translate(desiredLanguage: 'en' | 'es' | throw new Error(`${phraseKey} was not found in the default language`); } -/** - * Map to store translated values for each locale. - * This is used to avoid translating the same phrase multiple times. - * - * The data is stored in the following format: - * - * { - * "en": { - * "name": "Name", - * } - * - * Note: We are not storing any translated values for phrases with variables, - * as they have higher chance of being unique, so we'll end up wasting space - * in our cache. - */ -const translationCache = new Map, Map>( - Object.values(CONST.LOCALES).reduce((cache, locale) => { - cache.push([locale, new Map()]); - return cache; - }, [] as Array<[ValueOf, Map]>), -); - /** * Uses the locale in this file updated by the Onyx subscriber. */ function translateLocal(phrase: TKey, ...variables: PhraseParameters>) { - const preferredLocale = BaseLocaleListener.getPreferredLocale(); - const isVariablesEmpty = variables.length === 0; - - // Get the cache for the preferred locale - const cacheForLocale = translationCache.get(preferredLocale); - - // Directly access and assign the translated value from the cache, instead of - // going through map.has() and map.get() to avoid multiple lookups. - const valueFromCache = cacheForLocale?.get(phrase); - - // If the phrase is already translated, return the translated value - if (valueFromCache) { - return valueFromCache; - } - const translatedText = translate(preferredLocale, phrase, ...variables); - - // We don't want to store translated values for phrases with variables - if (isVariablesEmpty) { - // We set the translated value in the cache - cacheForLocale?.set(phrase, translatedText); - } - return translatedText; + return translate(BaseLocaleListener.getPreferredLocale(), phrase, ...variables); } /** From e22c22bf92dee2064fbe90b11ed7c11e0ae7d04f Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 14 Mar 2024 17:27:27 +0500 Subject: [PATCH 21/21] refactor: use dedicated function to get translated phrase to avoid duplication --- src/libs/Localize/index.ts | 74 +++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 27451e5296a3..0e65e5b8be87 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -73,17 +73,31 @@ const translationCache = new Map, Map(desiredLanguage: 'en' | 'es' | 'es-ES' | 'es_ES', phraseKey: TKey, ...phraseParameters: PhraseParameters>): string { - // Search phrase in full locale e.g. es-ES - const language = desiredLanguage === CONST.LOCALES.ES_ES_ONFIDO ? CONST.LOCALES.ES_ES : desiredLanguage; - +function getTranslatedPhrase( + language: 'en' | 'es' | 'es-ES', + phraseKey: TKey, + fallbackLanguage: 'en' | 'es' | null = null, + ...phraseParameters: PhraseParameters> +): string | null { // Get the cache for the above locale - let cacheForLocale = translationCache.get(language); + const cacheForLocale = translationCache.get(language); // Directly access and assign the translated value from the cache, instead of // going through map.has() and map.get() to avoid multiple lookups. @@ -94,7 +108,8 @@ function translate(desiredLanguage: 'en' | 'es' | return valueFromCache; } - let translatedPhrase = translations?.[language]?.[phraseKey] as Phrase; + const translatedPhrase = translations?.[language]?.[phraseKey] as Phrase; + if (translatedPhrase) { if (typeof translatedPhrase === 'function') { return translatedPhrase(...phraseParameters); @@ -105,29 +120,40 @@ function translate(desiredLanguage: 'en' | 'es' | return translatedPhrase; } + if (!fallbackLanguage) { + return null; + } + // Phrase is not found in full locale, search it in fallback language e.g. es - const languageAbbreviation = desiredLanguage.substring(0, 2) as 'en' | 'es'; - // Get the cache for the above locale - cacheForLocale = translationCache.get(languageAbbreviation); - translatedPhrase = translations?.[languageAbbreviation]?.[phraseKey] as Phrase; - if (translatedPhrase) { - if (typeof translatedPhrase === 'function') { - return translatedPhrase(...phraseParameters); - } + const fallbacktranslatedPhrase = getTranslatedPhrase(fallbackLanguage, phraseKey, null, ...phraseParameters); - // We set the translated value in the cache only for the phrases without parameters. - cacheForLocale?.set(phraseKey, translatedPhrase); - return translatedPhrase; + if (fallbacktranslatedPhrase) { + return fallbacktranslatedPhrase; } - if (languageAbbreviation !== CONST.LOCALES.DEFAULT) { - Log.alert(`${phraseKey} was not found in the ${languageAbbreviation} locale`); + if (fallbackLanguage !== CONST.LOCALES.DEFAULT) { + Log.alert(`${phraseKey} was not found in the ${fallbackLanguage} locale`); } // Phrase is not translated, search it in default language (en) - translatedPhrase = translations?.[CONST.LOCALES.DEFAULT]?.[phraseKey] as Phrase; + return getTranslatedPhrase(CONST.LOCALES.DEFAULT, phraseKey, null, ...phraseParameters); +} + +/** + * Return translated string for given locale and phrase + * + * @param [desiredLanguage] eg 'en', 'es-ES' + * @param [phraseParameters] Parameters to supply if the phrase is a template literal. + */ +function translate(desiredLanguage: 'en' | 'es' | 'es-ES' | 'es_ES', phraseKey: TKey, ...phraseParameters: PhraseParameters>): string { + // Search phrase in full locale e.g. es-ES + const language = desiredLanguage === CONST.LOCALES.ES_ES_ONFIDO ? CONST.LOCALES.ES_ES : desiredLanguage; + // Phrase is not found in full locale, search it in fallback language e.g. es + const languageAbbreviation = desiredLanguage.substring(0, 2) as 'en' | 'es'; + + const translatedPhrase = getTranslatedPhrase(language, phraseKey, languageAbbreviation, ...phraseParameters); if (translatedPhrase) { - return typeof translatedPhrase === 'function' ? translatedPhrase(...phraseParameters) : translatedPhrase; + return translatedPhrase; } // Phrase is not found in default language, on production and staging log an alert to server