From 07d64187afe2cd5f26b912ad8e8a84c5d5b2e83c Mon Sep 17 00:00:00 2001
From: Jakub Trzebiatowski <cubuspl42@gmail.com>
Date: Mon, 11 Dec 2023 20:21:34 +0100
Subject: [PATCH 1/2] Revert "Revert "translate invite member to room""

This reverts commit 9d31e4b9e7f85d17988cb1efd7cac6c48e700e46.
---
 src/languages/en.ts                           |   6 +
 src/languages/es.ts                           |   6 +
 src/libs/Localize/index.ts                    |  46 ++++++-
 src/libs/MessageElement.ts                    |  11 ++
 src/libs/PersonalDetailsUtils.js              |  13 ++
 src/libs/ReportActionsUtils.ts                | 113 +++++++++++++++++-
 src/libs/ReportUtils.ts                       |  41 +------
 src/libs/SidebarUtils.ts                      |  10 +-
 .../report/ContextMenu/ContextMenuActions.js  |   4 +-
 .../home/report/ReportActionItemMessage.js    |  15 +++
 src/types/onyx/OriginalMessage.ts             |   1 +
 tests/unit/LocalizeTests.js                   |   8 +-
 12 files changed, 213 insertions(+), 61 deletions(-)
 create mode 100644 src/libs/MessageElement.ts

diff --git a/src/languages/en.ts b/src/languages/en.ts
index bd2d941a705..557ccd176b8 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1552,6 +1552,12 @@ export default {
             invitePeople: 'Invite new members',
             genericFailureMessage: 'An error occurred inviting the user to the workspace, please try again.',
             pleaseEnterValidLogin: `Please ensure the email or phone number is valid (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`,
+            user: 'user',
+            users: 'users',
+            invited: 'invited',
+            removed: 'removed',
+            to: 'to',
+            from: 'from',
         },
         inviteMessage: {
             inviteMessageTitle: 'Add message',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 5c80ec93ac7..a7bf3cdc98b 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1574,6 +1574,12 @@ export default {
             invitePeople: 'Invitar nuevos miembros',
             genericFailureMessage: 'Se produjo un error al invitar al usuario al espacio de trabajo. Vuelva a intentarlo..',
             pleaseEnterValidLogin: `Asegúrese de que el correo electrónico o el número de teléfono sean válidos (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER}).`,
+            user: 'usuario',
+            users: 'usuarios',
+            invited: 'invitó',
+            removed: 'eliminó',
+            to: 'a',
+            from: 'de',
         },
         inviteMessage: {
             inviteMessageTitle: 'Añadir un mensaje',
diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts
index 488ff0d9b98..77c34ebdc57 100644
--- a/src/libs/Localize/index.ts
+++ b/src/libs/Localize/index.ts
@@ -1,6 +1,7 @@
 import * as RNLocalize from 'react-native-localize';
 import Onyx from 'react-native-onyx';
 import Log from '@libs/Log';
+import {MessageElementBase, MessageTextElement} from '@libs/MessageElement';
 import Config from '@src/CONFIG';
 import CONST from '@src/CONST';
 import translations from '@src/languages/translations';
@@ -121,15 +122,48 @@ function translateIfPhraseKey(message: MaybePhraseKey): string {
     }
 }
 
+function getPreferredListFormat(): Intl.ListFormat {
+    if (!CONJUNCTION_LIST_FORMATS_FOR_LOCALES) {
+        init();
+    }
+
+    return CONJUNCTION_LIST_FORMATS_FOR_LOCALES[BaseLocaleListener.getPreferredLocale()];
+}
+
 /**
  * Format an array into a string with comma and "and" ("a dog, a cat and a chicken")
  */
-function arrayToString(anArray: string[]) {
-    if (!CONJUNCTION_LIST_FORMATS_FOR_LOCALES) {
-        init();
+function formatList(components: string[]) {
+    const listFormat = getPreferredListFormat();
+    return listFormat.format(components);
+}
+
+function formatMessageElementList<E extends MessageElementBase>(elements: readonly E[]): ReadonlyArray<E | MessageTextElement> {
+    const listFormat = getPreferredListFormat();
+    const parts = listFormat.formatToParts(elements.map((e) => e.content));
+    const resultElements: Array<E | MessageTextElement> = [];
+
+    let nextElementIndex = 0;
+    for (const part of parts) {
+        if (part.type === 'element') {
+            /**
+             * The standard guarantees that all input elements will be present in the constructed parts, each exactly
+             * once, and without any modifications: https://tc39.es/ecma402/#sec-createpartsfromlist
+             */
+            const element = elements[nextElementIndex++];
+
+            resultElements.push(element);
+        } else {
+            const literalElement: MessageTextElement = {
+                kind: 'text',
+                content: part.value,
+            };
+
+            resultElements.push(literalElement);
+        }
     }
-    const listFormat = CONJUNCTION_LIST_FORMATS_FOR_LOCALES[BaseLocaleListener.getPreferredLocale()];
-    return listFormat.format(anArray);
+
+    return resultElements;
 }
 
 /**
@@ -139,5 +173,5 @@ function getDevicePreferredLocale(): string {
     return RNLocalize.findBestAvailableLanguage([CONST.LOCALES.EN, CONST.LOCALES.ES])?.languageTag ?? CONST.LOCALES.DEFAULT;
 }
 
-export {translate, translateLocal, translateIfPhraseKey, arrayToString, getDevicePreferredLocale};
+export {translate, translateLocal, translateIfPhraseKey, formatList, formatMessageElementList, getDevicePreferredLocale};
 export type {PhraseParameters, Phrase, MaybePhraseKey};
diff --git a/src/libs/MessageElement.ts b/src/libs/MessageElement.ts
new file mode 100644
index 00000000000..584d7e1e289
--- /dev/null
+++ b/src/libs/MessageElement.ts
@@ -0,0 +1,11 @@
+type MessageElementBase = {
+    readonly kind: string;
+    readonly content: string;
+};
+
+type MessageTextElement = {
+    readonly kind: 'text';
+    readonly content: string;
+} & MessageElementBase;
+
+export type {MessageElementBase, MessageTextElement};
diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js
index 560480dcec9..8a415139145 100644
--- a/src/libs/PersonalDetailsUtils.js
+++ b/src/libs/PersonalDetailsUtils.js
@@ -197,6 +197,18 @@ function getFormattedAddress(privatePersonalDetails) {
     return formattedAddress.trim().replace(/,$/, '');
 }
 
+/**
+ * @param {Object} personalDetail - details object
+ * @returns {String | undefined} - The effective display name
+ */
+function getEffectiveDisplayName(personalDetail) {
+    if (personalDetail) {
+        return LocalePhoneNumber.formatPhoneNumber(personalDetail.login) || personalDetail.displayName;
+    }
+
+    return undefined;
+}
+
 export {
     getDisplayNameOrDefault,
     getPersonalDetailsByIDs,
@@ -206,4 +218,5 @@ export {
     getFormattedAddress,
     getFormattedStreet,
     getStreetLines,
+    getEffectiveDisplayName,
 };
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 6ddc2ac99e0..21c382346e5 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -5,14 +5,17 @@ import OnyxUtils from 'react-native-onyx/lib/utils';
 import {ValueOf} from 'type-fest';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
-import {ActionName} from '@src/types/onyx/OriginalMessage';
+import {ActionName, ChangeLog} from '@src/types/onyx/OriginalMessage';
 import Report from '@src/types/onyx/Report';
-import ReportAction, {ReportActions} from '@src/types/onyx/ReportAction';
+import ReportAction, {Message, ReportActions} from '@src/types/onyx/ReportAction';
 import {EmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject';
 import * as CollectionUtils from './CollectionUtils';
 import * as Environment from './Environment/Environment';
 import isReportMessageAttachment from './isReportMessageAttachment';
+import * as Localize from './Localize';
 import Log from './Log';
+import {MessageElementBase, MessageTextElement} from './MessageElement';
+import * as PersonalDetailsUtils from './PersonalDetailsUtils';
 
 type LastVisibleMessage = {
     lastMessageTranslationKey?: string;
@@ -20,6 +23,19 @@ type LastVisibleMessage = {
     lastMessageHtml?: string;
 };
 
+type MemberChangeMessageUserMentionElement = {
+    readonly kind: 'userMention';
+    readonly accountID: number;
+} & MessageElementBase;
+
+type MemberChangeMessageRoomReferenceElement = {
+    readonly kind: 'roomReference';
+    readonly roomName: string;
+    readonly roomID: number;
+} & MessageElementBase;
+
+type MemberChangeMessageElement = MessageTextElement | MemberChangeMessageUserMentionElement | MemberChangeMessageRoomReferenceElement;
+
 const allReports: OnyxCollection<Report> = {};
 Onyx.connect({
     key: ONYXKEYS.COLLECTION.REPORT,
@@ -104,7 +120,7 @@ function isReimbursementQueuedAction(reportAction: OnyxEntry<ReportAction>) {
     return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED;
 }
 
-function isChannelLogMemberAction(reportAction: OnyxEntry<ReportAction>) {
+function isMemberChangeAction(reportAction: OnyxEntry<ReportAction>) {
     return (
         reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM ||
         reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.REMOVE_FROM_ROOM ||
@@ -113,6 +129,10 @@ function isChannelLogMemberAction(reportAction: OnyxEntry<ReportAction>) {
     );
 }
 
+function isInviteMemberAction(reportAction: OnyxEntry<ReportAction>) {
+    return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM;
+}
+
 function isReimbursementDeQueuedAction(reportAction: OnyxEntry<ReportAction>): boolean {
     return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED;
 }
@@ -653,6 +673,89 @@ function isNotifiableReportAction(reportAction: OnyxEntry<ReportAction>): boolea
     return actions.includes(reportAction.actionName);
 }
 
+function getMemberChangeMessageElements(reportAction: OnyxEntry<ReportAction>): readonly MemberChangeMessageElement[] {
+    const isInviteAction = isInviteMemberAction(reportAction);
+
+    // Currently, we only render messages when members are invited
+    const verb = isInviteAction ? Localize.translateLocal('workspace.invite.invited') : Localize.translateLocal('workspace.invite.removed');
+
+    const originalMessage = reportAction?.originalMessage as ChangeLog;
+    const targetAccountIDs: number[] = originalMessage?.targetAccountIDs ?? [];
+    const personalDetails = PersonalDetailsUtils.getPersonalDetailsByIDs(targetAccountIDs, 0);
+
+    const mentionElements = targetAccountIDs.map((accountID): MemberChangeMessageUserMentionElement => {
+        const personalDetail = personalDetails.find((personal) => personal.accountID === accountID);
+        const handleText = PersonalDetailsUtils.getEffectiveDisplayName(personalDetail) ?? Localize.translateLocal('common.hidden');
+
+        return {
+            kind: 'userMention',
+            content: `@${handleText}`,
+            accountID,
+        };
+    });
+
+    const buildRoomElements = (): readonly MemberChangeMessageElement[] => {
+        const roomName = originalMessage?.roomName;
+
+        if (roomName) {
+            const preposition = isInviteAction ? ` ${Localize.translateLocal('workspace.invite.to')} ` : ` ${Localize.translateLocal('workspace.invite.from')} `;
+
+            if (originalMessage.reportID) {
+                return [
+                    {
+                        kind: 'text',
+                        content: preposition,
+                    },
+                    {
+                        kind: 'roomReference',
+                        roomName,
+                        roomID: originalMessage.reportID,
+                        content: roomName,
+                    },
+                ];
+            }
+        }
+
+        return [];
+    };
+
+    return [
+        {
+            kind: 'text',
+            content: `${verb} `,
+        },
+        ...Localize.formatMessageElementList(mentionElements),
+        ...buildRoomElements(),
+    ];
+}
+
+function getMemberChangeMessageFragment(reportAction: OnyxEntry<ReportAction>): Message {
+    const messageElements: readonly MemberChangeMessageElement[] = getMemberChangeMessageElements(reportAction);
+    const html = messageElements
+        .map((messageElement) => {
+            switch (messageElement.kind) {
+                case 'userMention':
+                    return `<mention-user accountID=${messageElement.accountID}></mention-user>`;
+                case 'roomReference':
+                    return `<a href="${environmentURL}/r/${messageElement.roomID}" target="_blank">${messageElement.roomName}</a>`;
+                default:
+                    return messageElement.content;
+            }
+        })
+        .join('');
+
+    return {
+        html: `<muted-text>${html}</muted-text>`,
+        text: reportAction?.message ? reportAction?.message[0].text : '',
+        type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
+    };
+}
+
+function getMemberChangeMessagePlainText(reportAction: OnyxEntry<ReportAction>): string {
+    const messageElements = getMemberChangeMessageElements(reportAction);
+    return messageElements.map((element) => element.content).join('');
+}
+
 /**
  * Helper method to determine if the provided accountID has made a request on the specified report.
  *
@@ -716,7 +819,9 @@ export {
     shouldReportActionBeVisibleAsLastAction,
     hasRequestFromCurrentAccount,
     getFirstVisibleReportActionID,
-    isChannelLogMemberAction,
+    isMemberChangeAction,
+    getMemberChangeMessageFragment,
+    getMemberChangeMessagePlainText,
     isReimbursementDeQueuedAction,
 };
 
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 9e51d35fb25..94271da2f07 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -18,7 +18,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
 import {Beta, Login, PersonalDetails, Policy, PolicyTags, Report, ReportAction, Session, Transaction} from '@src/types/onyx';
 import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
-import {ChangeLog, IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage';
+import {IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage';
 import {Message, ReportActions} from '@src/types/onyx/ReportAction';
 import {Receipt, WaypointCollection} from '@src/types/onyx/Transaction';
 import DeepValueOf from '@src/types/utils/DeepValueOf';
@@ -4191,44 +4191,6 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry<ReportAction>)
     });
 }
 
-/**
- * Return room channel log display message
- */
-function getChannelLogMemberMessage(reportAction: OnyxEntry<ReportAction>): string {
-    const verb =
-        reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM
-            ? 'invited'
-            : 'removed';
-
-    const mentions = (reportAction?.originalMessage as ChangeLog)?.targetAccountIDs?.map(() => {
-        const personalDetail = allPersonalDetails?.accountID;
-        const displayNameOrLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '') || (personalDetail?.displayName ?? '') || Localize.translateLocal('common.hidden');
-        return `@${displayNameOrLogin}`;
-    });
-
-    const lastMention = mentions?.pop();
-    let message = '';
-
-    if (mentions?.length === 0) {
-        message = `${verb} ${lastMention}`;
-    } else if (mentions?.length === 1) {
-        message = `${verb} ${mentions?.[0]} and ${lastMention}`;
-    } else {
-        message = `${verb} ${mentions?.join(', ')}, and ${lastMention}`;
-    }
-
-    const roomName = (reportAction?.originalMessage as ChangeLog)?.roomName ?? '';
-    if (roomName) {
-        const preposition =
-            reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM
-                ? ' to'
-                : ' from';
-        message += `${preposition} ${roomName}`;
-    }
-
-    return message;
-}
-
 /**
  * Checks if a report is a group chat.
  *
@@ -4479,7 +4441,6 @@ export {
     getReimbursementQueuedActionMessage,
     getReimbursementDeQueuedActionMessage,
     getPersonalDetailsForAccountID,
-    getChannelLogMemberMessage,
     getRoom,
     shouldDisableWelcomeMessage,
     navigateToPrivateNotes,
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 1da1469a268..82b73d3ceec 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -373,17 +373,17 @@ function getOptionData(
             const targetAccountIDs = lastAction?.originalMessage?.targetAccountIDs ?? [];
             const verb =
                 lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM
-                    ? 'invited'
-                    : 'removed';
-            const users = targetAccountIDs.length > 1 ? 'users' : 'user';
+                    ? Localize.translate(preferredLocale, 'workspace.invite.invited')
+                    : Localize.translate(preferredLocale, 'workspace.invite.removed');
+            const users = Localize.translate(preferredLocale, targetAccountIDs.length > 1 ? 'workspace.invite.users' : 'workspace.invite.user');
             result.alternateText = `${verb} ${targetAccountIDs.length} ${users}`;
 
             const roomName = lastAction?.originalMessage?.roomName ?? '';
             if (roomName) {
                 const preposition =
                     lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM
-                        ? ' to'
-                        : ' from';
+                        ? ` ${Localize.translate(preferredLocale, 'workspace.invite.to')}`
+                        : ` ${Localize.translate(preferredLocale, 'workspace.invite.from')}`;
                 result.alternateText += `${preposition} ${roomName}`;
             }
         } else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastActorDisplayName && lastMessageTextFromReport) {
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js
index 2d9faa574eb..5e6f2d46abd 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.js
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js
@@ -283,8 +283,8 @@ export default [
                 } else if (ReportActionsUtils.isCreatedTaskReportAction(reportAction)) {
                     const taskPreviewMessage = TaskUtils.getTaskCreatedMessage(reportAction);
                     Clipboard.setString(taskPreviewMessage);
-                } else if (ReportActionsUtils.isChannelLogMemberAction(reportAction)) {
-                    const logMessage = ReportUtils.getChannelLogMemberMessage(reportAction);
+                } else if (ReportActionsUtils.isMemberChangeAction(reportAction)) {
+                    const logMessage = ReportActionsUtils.getMemberChangeMessagePlainText(reportAction);
                     Clipboard.setString(logMessage);
                 } else if (ReportActionsUtils.isSubmittedExpenseAction(reportAction)) {
                     const submittedMessage = _.reduce(reportAction.message, (acc, curr) => `${acc}${curr.text}`, '');
diff --git a/src/pages/home/report/ReportActionItemMessage.js b/src/pages/home/report/ReportActionItemMessage.js
index 2265530f29a..46e0438f250 100644
--- a/src/pages/home/report/ReportActionItemMessage.js
+++ b/src/pages/home/report/ReportActionItemMessage.js
@@ -8,6 +8,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils';
 import * as ReportUtils from '@libs/ReportUtils';
 import useThemeStyles from '@styles/useThemeStyles';
 import CONST from '@src/CONST';
+import TextCommentFragment from './comment/TextCommentFragment';
 import ReportActionItemFragment from './ReportActionItemFragment';
 import reportActionPropTypes from './reportActionPropTypes';
 
@@ -40,6 +41,20 @@ function ReportActionItemMessage(props) {
     const styles = useThemeStyles();
     const fragments = _.compact(props.action.previousMessage || props.action.message);
     const isIOUReport = ReportActionsUtils.isMoneyRequestAction(props.action);
+    if (ReportActionsUtils.isMemberChangeAction(props.action)) {
+        const fragment = ReportActionsUtils.getMemberChangeMessageFragment(props.action);
+
+        return (
+            <TextCommentFragment
+                fragment={fragment}
+                displayAsGroup={props.displayAsGroup}
+                style={props.style}
+                source=""
+                styleAsDeleted={false}
+            />
+        );
+    }
+
     let iouMessage;
     if (isIOUReport) {
         const iouReportID = lodashGet(props.action, 'originalMessage.IOUReportID');
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index f76fbd5ffd7..72ea275e3ba 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -140,6 +140,7 @@ type ChronosOOOTimestamp = {
 type ChangeLog = {
     targetAccountIDs?: number[];
     roomName?: string;
+    reportID?: number;
 };
 
 type ChronosOOOEvent = {
diff --git a/tests/unit/LocalizeTests.js b/tests/unit/LocalizeTests.js
index 4c89d587fc0..7693a0a4a88 100644
--- a/tests/unit/LocalizeTests.js
+++ b/tests/unit/LocalizeTests.js
@@ -15,7 +15,7 @@ describe('localize', () => {
 
     afterEach(() => Onyx.clear());
 
-    describe('arrayToString', () => {
+    describe('formatList', () => {
         test.each([
             [
                 [],
@@ -52,9 +52,9 @@ describe('localize', () => {
                     [CONST.LOCALES.ES]: 'rory, vit e ionatan',
                 },
             ],
-        ])('arrayToSpokenList(%s)', (input, {[CONST.LOCALES.DEFAULT]: expectedOutput, [CONST.LOCALES.ES]: expectedOutputES}) => {
-            expect(Localize.arrayToString(input)).toBe(expectedOutput);
-            return Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES).then(() => expect(Localize.arrayToString(input)).toBe(expectedOutputES));
+        ])('formatList(%s)', (input, {[CONST.LOCALES.DEFAULT]: expectedOutput, [CONST.LOCALES.ES]: expectedOutputES}) => {
+            expect(Localize.formatList(input)).toBe(expectedOutput);
+            return Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES).then(() => expect(Localize.formatList(input)).toBe(expectedOutputES));
         });
     });
 });

From 09f457e0951977d108faabc6d7aa628de3112177 Mon Sep 17 00:00:00 2001
From: Nam Le <yentth237@gmail.com>
Date: Wed, 6 Dec 2023 23:43:56 +0700
Subject: [PATCH 2/2] Fix the circural import

---
 src/languages/en.ts | 7 +++----
 src/languages/es.ts | 8 ++++----
 2 files changed, 7 insertions(+), 8 deletions(-)

diff --git a/src/languages/en.ts b/src/languages/en.ts
index 557ccd176b8..6eadaccda37 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1,5 +1,4 @@
 import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
-import * as ReportActionsUtils from '@libs/ReportActionsUtils';
 import CONST from '@src/CONST';
 import type {
     AddressLineParams,
@@ -426,9 +425,9 @@ export default {
         copyEmailToClipboard: 'Copy email to clipboard',
         markAsUnread: 'Mark as unread',
         markAsRead: 'Mark as read',
-        editAction: ({action}: EditActionParams) => `Edit ${ReportActionsUtils.isMoneyRequestAction(action) ? 'request' : 'comment'}`,
-        deleteAction: ({action}: DeleteActionParams) => `Delete ${ReportActionsUtils.isMoneyRequestAction(action) ? 'request' : 'comment'}`,
-        deleteConfirmation: ({action}: DeleteConfirmationParams) => `Are you sure you want to delete this ${ReportActionsUtils.isMoneyRequestAction(action) ? 'request' : 'comment'}?`,
+        editAction: ({action}: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'request' : 'comment'}`,
+        deleteAction: ({action}: DeleteActionParams) => `Delete ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'request' : 'comment'}`,
+        deleteConfirmation: ({action}: DeleteConfirmationParams) => `Are you sure you want to delete this ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'request' : 'comment'}?`,
         onlyVisible: 'Only visible to',
         replyInThread: 'Reply in thread',
         subscribeToThread: 'Subscribe to thread',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index a7bf3cdc98b..3f132e470f6 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1,4 +1,3 @@
-import * as ReportActionsUtils from '@libs/ReportActionsUtils';
 import CONST from '@src/CONST';
 import type {
     AddressLineParams,
@@ -417,9 +416,10 @@ export default {
         copyEmailToClipboard: 'Copiar email al portapapeles',
         markAsUnread: 'Marcar como no leído',
         markAsRead: 'Marcar como leído',
-        editAction: ({action}: EditActionParams) => `Edit ${ReportActionsUtils.isMoneyRequestAction(action) ? 'pedido' : 'comentario'}`,
-        deleteAction: ({action}: DeleteActionParams) => `Eliminar ${ReportActionsUtils.isMoneyRequestAction(action) ? 'pedido' : 'comentario'}`,
-        deleteConfirmation: ({action}: DeleteConfirmationParams) => `¿Estás seguro de que quieres eliminar este ${ReportActionsUtils.isMoneyRequestAction(action) ? 'pedido' : 'comentario'}`,
+        editAction: ({action}: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'pedido' : 'comentario'}`,
+        deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'pedido' : 'comentario'}`,
+        deleteConfirmation: ({action}: DeleteConfirmationParams) =>
+            `¿Estás seguro de que quieres eliminar este ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'pedido' : 'comentario'}`,
         onlyVisible: 'Visible sólo para',
         replyInThread: 'Responder en el hilo',
         subscribeToThread: 'Suscribirse al hilo',
