From 019c7bfbb3a7ce8bc06b23e2bd37a69252670135 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 4 May 2026 17:44:43 -0300 Subject: [PATCH 01/43] add policy change log --- src/CONST/index.ts | 3 + src/libs/ReportActionsUtils.ts | 219 ++++++++++++++++++ src/libs/SidebarUtils.ts | 9 + .../report/ContextMenu/ContextMenuActions.tsx | 9 + .../actionContents/PolicyChangeLogContent.tsx | 6 + src/types/onyx/OriginalMessage.ts | 66 ++++++ 6 files changed, 312 insertions(+) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 6cfe90209277..de2d57b12887 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1525,6 +1525,7 @@ const CONST = { ADD_CUSTOM_UNIT_RATE: 'POLICYCHANGELOG_ADD_CUSTOM_UNIT_RATE', ADD_EMPLOYEE: 'POLICYCHANGELOG_ADD_EMPLOYEE', ADD_CARD_FEED: 'POLICYCHANGELOG_ADD_CARD_FEED', + ADD_EXPENSIFY_CARD_RULE: 'POLICYCHANGELOG_ADD_EXPENSIFY_CARD_RULE', ADD_INTEGRATION: 'POLICYCHANGELOG_ADD_INTEGRATION', ADD_REPORT_FIELD: 'POLICYCHANGELOG_ADD_REPORT_FIELD', ADD_TAG: 'POLICYCHANGELOG_ADD_TAG', @@ -1550,6 +1551,7 @@ const CONST = { INDIVIDUAL_BUDGET_NOTIFICATION: 'POLICYCHANGELOG_INDIVIDUAL_BUDGET_NOTIFICATION', INVITE_TO_ROOM: 'POLICYCHANGELOG_INVITETOROOM', REMOVE_FROM_ROOM: 'POLICYCHANGELOG_REMOVEFROMROOM', + REMOVE_EXPENSIFY_CARD_RULE: 'POLICYCHANGELOG_REMOVE_EXPENSIFY_CARD_RULE', LEAVE_ROOM: 'POLICYCHANGELOG_LEAVEROOM', REPLACE_CATEGORIES: 'POLICYCHANGELOG_REPLACE_CATEGORIES', SET_AUTO_REIMBURSEMENT: 'POLICYCHANGELOG_SET_AUTOREIMBURSEMENT', @@ -1576,6 +1578,7 @@ const CONST = { UPDATE_DEFAULT_TITLE_ENFORCED: 'POLICYCHANGELOG_UPDATE_DEFAULT_TITLE_ENFORCED', UPDATE_DISABLED_FIELDS: 'POLICYCHANGELOG_UPDATE_DISABLED_FIELDS', UPDATE_EMPLOYEE: 'POLICYCHANGELOG_UPDATE_EMPLOYEE', + UPDATE_EXPENSIFY_CARD_RULE: 'POLICYCHANGELOG_UPDATE_EXPENSIFY_CARD_RULE', UPDATE_FIELD: 'POLICYCHANGELOG_UPDATE_FIELD', UPDATE_ADDRESS: 'POLICYCHANGELOG_UPDATE_ADDRESS', UPDATE_FEATURE_ENABLED: 'POLICYCHANGELOG_UPDATE_FEATURE_ENABLED', diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 0035ac6e13ec..33352b25e862 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -33,6 +33,7 @@ import type { OriginalMessageChangeLog, OriginalMessageExportIntegration, OriginalMessageMarkedReimbursed, + OriginalMessagePolicyChangeLog, OriginalMessageReimbursed, OriginalMessageUnreportedTransaction, PolicyBudgetFrequency, @@ -3905,6 +3906,221 @@ function getUpdatedApprovalRuleMessage(translate: LocalizedTranslate, reportActi return getReportActionText(reportAction); } +/** Mirrors Web-Expensify `Report_Action_PolicyChangeLog_SpendRuleMessage::preformattedText` */ +function getSpendRulePreformattedText(message: OriginalMessagePolicyChangeLog): string | undefined { + for (const key of ['changeLogText', 'text', 'displayMessage'] as const) { + const value = message?.[key]; + if (typeof value === 'string' && value !== '') { + return value; + } + } + return undefined; +} + +function cardCountFromSpendRuleMessage(message: OriginalMessagePolicyChangeLog): number { + const raw = message?.cardCount; + if (typeof raw === 'number' && Number.isFinite(raw)) { + return raw; + } + if (typeof raw === 'string' && raw !== '' && /^\d+$/.test(raw)) { + return Number.parseInt(raw, 10); + } + return 0; +} + +function spendRuleCardScopeFragment(message: OriginalMessagePolicyChangeLog): string { + const cardCount = cardCountFromSpendRuleMessage(message); + const cardName = typeof message?.cardName === 'string' ? message.cardName : ''; + if (cardCount > 1) { + return `${cardCount} cards`; + } + if (cardName !== '') { + return cardName; + } + if (cardCount === 1) { + return '1 card'; + } + let lastFour = typeof message?.cardLastFour === 'string' ? message.cardLastFour : ''; + if (lastFour === '' && typeof message?.lastFour === 'string') { + lastFour = message.lastFour; + } + if (lastFour !== '') { + return `card ending in ${lastFour}`; + } + return ''; +} + +function normalizeSpendRuleEffect(raw: string): 'allow' | 'block' | '' { + const v = raw.toLowerCase(); + if (v === 'allow' || v === 'block') { + return v; + } + return ''; +} + +function normalizeSpendRuleModeLabel(raw: string): string { + const v = raw.toLowerCase(); + if (v === 'allow' || v === 'block') { + return v; + } + return ''; +} + +function normalizeSpendRuleMaxAmountComparison(raw: string): 'under' | 'over' | '' { + const v = raw.toLowerCase(); + if (v === 'under' || v === 'over') { + return v; + } + return ''; +} + +function mixDescriptionFromSpendRuleMessage(message: OriginalMessagePolicyChangeLog): string { + if (typeof message?.mixDescription === 'string' && message.mixDescription !== '') { + return message.mixDescription; + } + const mixParts = message?.mixParts; + if (!Array.isArray(mixParts)) { + return ''; + } + const parts: string[] = []; + for (const part of mixParts) { + if (typeof part === 'string' && part !== '') { + parts.push(part); + } + } + const n = parts.length; + if (n === 0) { + return ''; + } + if (n === 1) { + return parts.at(0) ?? ''; + } + const last = parts.pop(); + return `${parts.join(', ')}, and ${last}`; +} + +function spendRuleFallbackAddOrUpdate(message: OriginalMessagePolicyChangeLog, isAdd: boolean): string { + const verb = isAdd ? 'added' : 'updated'; + const onCard = spendRuleCardScopeFragment(message); + if (onCard !== '') { + return `${verb} spend rule on ${onCard}`; + } + return `${verb} spend rule`; +} + +/** Mirrors Web-Expensify `Report_Action_PolicyChangeLog_SpendRuleMessage::buildAddOrUpdate` (structured path only; caller handles preformatted text). */ +function spendRuleBuildAddOrUpdate(message: OriginalMessagePolicyChangeLog, isAdd: boolean): string { + const effect = normalizeSpendRuleEffect(String(message?.effect ?? message?.ruleEffect ?? '')); + if (effect === '') { + return spendRuleFallbackAddOrUpdate(message, isAdd); + } + + const verb = effect === 'allow' ? 'allowed' : 'blocked'; + const summaryTypeRaw = message?.summaryType ?? message?.spendRuleSummaryType ?? ''; + const summaryType = typeof summaryTypeRaw === 'string' ? summaryTypeRaw : ''; + const onCard = spendRuleCardScopeFragment(message); + const onSuffix = onCard !== '' ? ` on ${onCard}` : ''; + + switch (summaryType) { + case 'merchant': { + const merchant = typeof message?.merchant === 'string' ? message.merchant : ''; + if (merchant === '') { + return spendRuleFallbackAddOrUpdate(message, isAdd); + } + return `${verb} ${merchant}${onSuffix}`; + } + case 'category': { + const category = typeof message?.category === 'string' ? message.category : ''; + if (category === '') { + return spendRuleFallbackAddOrUpdate(message, isAdd); + } + return `${verb} ${category}${onSuffix}`; + } + case 'max_amount': { + const comparison = normalizeSpendRuleMaxAmountComparison(typeof message?.maxAmountComparison === 'string' ? message.maxAmountComparison : ''); + const display = typeof message?.maxAmountDisplay === 'string' ? message.maxAmountDisplay : ''; + if (comparison === '' || display === '') { + return spendRuleFallbackAddOrUpdate(message, isAdd); + } + return `${verb} amounts ${comparison} ${display}${onSuffix}`; + } + case 'mix': { + const mix = mixDescriptionFromSpendRuleMessage(message); + if (mix === '') { + return spendRuleFallbackAddOrUpdate(message, isAdd); + } + return `${verb} ${mix.replace(/\.\s*$/, '')}.`; + } + default: + return spendRuleFallbackAddOrUpdate(message, isAdd); + } +} + +function isSpendRuleModeOnlyChange(message: OriginalMessagePolicyChangeLog): boolean { + const kind = message?.spendRuleChangeKind ?? message?.changeType ?? ''; + return typeof kind === 'string' && kind.toLowerCase() === 'mode'; +} + +function spendRuleBuildModeChange(message: OriginalMessagePolicyChangeLog): string { + const from = normalizeSpendRuleModeLabel(typeof message?.fromSpendRuleMode === 'string' ? message.fromSpendRuleMode : String(message?.fromMode ?? '')); + const to = normalizeSpendRuleModeLabel(typeof message?.toSpendRuleMode === 'string' ? message.toSpendRuleMode : String(message?.toMode ?? '')); + const onCard = spendRuleCardScopeFragment(message); + if (from !== '' && to !== '' && onCard !== '') { + return `changed spend rule from ${from} to ${to} on ${onCard}`; + } + if (from !== '' && to !== '') { + return `changed spend rule from ${from} to ${to}`; + } + return 'updated spend rule'; +} + +function spendRuleBuildRemove(message: OriginalMessagePolicyChangeLog): string { + const scope = spendRuleCardScopeFragment(message); + if (scope !== '') { + return `removed spend rule from ${scope}`; + } + return 'removed spend rule'; +} + +function getAddExpensifyCardRuleMessage(_translate: LocalizedTranslate, reportAction: OnyxEntry): string { + if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE)) { + return ''; + } + const message = getOriginalMessage(reportAction) ?? {}; + const pre = getSpendRulePreformattedText(message); + if (pre) { + return pre; + } + return spendRuleBuildAddOrUpdate(message, true); +} + +function getUpdateExpensifyCardRuleMessage(_translate: LocalizedTranslate, reportAction: OnyxEntry): string { + if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE)) { + return ''; + } + const message = getOriginalMessage(reportAction) ?? {}; + const pre = getSpendRulePreformattedText(message); + if (pre) { + return pre; + } + if (isSpendRuleModeOnlyChange(message)) { + return spendRuleBuildModeChange(message); + } + return spendRuleBuildAddOrUpdate(message, false); +} + +function getRemoveExpensifyCardRuleMessage(_translate: LocalizedTranslate, reportAction: OnyxEntry): string { + if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE)) { + return ''; + } + const message = getOriginalMessage(reportAction) ?? {}; + const pre = getSpendRulePreformattedText(message); + if (pre) { + return pre; + } + return spendRuleBuildRemove(message); +} + function getRemovedFromApprovalChainMessage(translate: LocalizedTranslate, reportAction: OnyxEntry>) { const originalMessage = getOriginalMessage(reportAction); const submittersNames = getPersonalDetailsByIDs({ @@ -4581,6 +4797,7 @@ export { getOneTransactionThreadReportAction, getOneTransactionThreadReportID, getOriginalMessage, + getAddExpensifyCardRuleMessage, getAddedApprovalRuleMessage, getDeletedApprovalRuleMessage, getUpdatedApprovalRuleMessage, @@ -4592,8 +4809,10 @@ export { getReportActionMessage, getReportActionMessageText, getReportActionText, + getRemoveExpensifyCardRuleMessage, getSortedReportActions, getSortedReportActionsForDisplay, + getUpdateExpensifyCardRuleMessage, isCardBrokenConnectionAction, getCardConnectionBrokenMessage, getTextFromHtml, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index b4068d56f90c..1b3aca2c13d0 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -42,6 +42,7 @@ import { getAddedBudgetMessage, getAddedCardFeedMessage, getAddedConnectionMessage, + getAddExpensifyCardRuleMessage, getAssignedCompanyCardMessage, getAutoPayApprovedReportsEnabledMessage, getAutoReimbursementMessage, @@ -78,6 +79,7 @@ import { getReimburserUpdateMessage, getRemovedCardFeedMessage, getRemovedConnectionMessage, + getRemoveExpensifyCardRuleMessage, getRenamedAction, getRenamedCardFeedMessage, getReportAction, @@ -109,6 +111,7 @@ import { getUpdatedSharedBudgetNotificationMessage, getUpdatedTimeEnabledMessage, getUpdatedTimeRateMessage, + getUpdateExpensifyCardRuleMessage, getUpdateRoomDescriptionMessage, getWorkspaceAttendeeTrackingUpdateMessage, getWorkspaceCategoriesUpdatedMessage, @@ -1161,6 +1164,12 @@ function getOptionData({ result.alternateText = getDeletedApprovalRuleMessage(translate, lastAction); } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_APPROVER_RULE) { result.alternateText = getUpdatedApprovalRuleMessage(translate, lastAction); + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE) { + result.alternateText = getAddExpensifyCardRuleMessage(translate, lastAction); + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE) { + result.alternateText = getUpdateExpensifyCardRuleMessage(translate, lastAction); + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE) { + result.alternateText = getRemoveExpensifyCardRuleMessage(translate, lastAction); } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MANUAL_APPROVAL_THRESHOLD) { result.alternateText = getUpdatedManualApprovalThresholdMessage(translate, lastAction); } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_BUDGET) { diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx index ca78eb8814d1..f5219c7df122 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx @@ -33,6 +33,7 @@ import { getAddedBudgetMessage, getAddedCardFeedMessage, getAddedConnectionMessage, + getAddExpensifyCardRuleMessage, getAssignedCompanyCardMessage, getAutoPayApprovedReportsEnabledMessage, getAutoReimbursementMessage, @@ -77,6 +78,7 @@ import { getReimburserUpdateMessage, getRemovedCardFeedMessage, getRemovedConnectionMessage, + getRemoveExpensifyCardRuleMessage, getRenamedAction, getRenamedCardFeedMessage, getReportAction, @@ -108,6 +110,7 @@ import { getUpdatedSharedBudgetNotificationMessage, getUpdatedTimeEnabledMessage, getUpdatedTimeRateMessage, + getUpdateExpensifyCardRuleMessage, getUpdateRoomDescriptionMessage, getWorkspaceAttendeeTrackingUpdateMessage, getWorkspaceCategoriesUpdatedMessage, @@ -1121,6 +1124,12 @@ const ContextMenuActions: ContextMenuAction[] = [ setClipboardMessage(getDeletedApprovalRuleMessage(translate, reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_APPROVER_RULE)) { setClipboardMessage(getUpdatedApprovalRuleMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE)) { + setClipboardMessage(getAddExpensifyCardRuleMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE)) { + setClipboardMessage(getUpdateExpensifyCardRuleMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE)) { + setClipboardMessage(getRemoveExpensifyCardRuleMessage(translate, reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MANUAL_APPROVAL_THRESHOLD)) { setClipboardMessage(getUpdatedManualApprovalThresholdMessage(translate, reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_BUDGET)) { diff --git a/src/pages/inbox/report/actionContents/PolicyChangeLogContent.tsx b/src/pages/inbox/report/actionContents/PolicyChangeLogContent.tsx index 63ac39058150..5d20c3f7ed78 100644 --- a/src/pages/inbox/report/actionContents/PolicyChangeLogContent.tsx +++ b/src/pages/inbox/report/actionContents/PolicyChangeLogContent.tsx @@ -10,6 +10,7 @@ import { getAddedBudgetMessage, getAddedCardFeedMessage, getAddedConnectionMessage, + getAddExpensifyCardRuleMessage, getAssignedCompanyCardMessage, getAutoPayApprovedReportsEnabledMessage, getAutoReimbursementMessage, @@ -36,6 +37,7 @@ import { getReimburserUpdateMessage, getRemovedCardFeedMessage, getRemovedConnectionMessage, + getRemoveExpensifyCardRuleMessage, getRenamedCardFeedMessage, getRequireCompanyCardsEnabledMessage, getSetAutoJoinMessage, @@ -60,6 +62,7 @@ import { getUpdatedSharedBudgetNotificationMessage, getUpdatedTimeEnabledMessage, getUpdatedTimeRateMessage, + getUpdateExpensifyCardRuleMessage, getWorkspaceAttendeeTrackingUpdateMessage, getWorkspaceCategoriesUpdatedMessage, getWorkspaceCategoryUpdateMessage, @@ -163,6 +166,9 @@ const POLICY_CHANGE_LOG_RESOLVERS: Record = { [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_APPROVER_RULE]: (translate, action) => getAddedApprovalRuleMessage(translate, action), [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_APPROVER_RULE]: (translate, action) => getDeletedApprovalRuleMessage(translate, action), [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_APPROVER_RULE]: (translate, action) => getUpdatedApprovalRuleMessage(translate, action), + [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE]: (translate, action) => getAddExpensifyCardRuleMessage(translate, action), + [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE]: (translate, action) => getUpdateExpensifyCardRuleMessage(translate, action), + [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE]: (translate, action) => getRemoveExpensifyCardRuleMessage(translate, action), [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_INTEGRATION]: (translate, action) => getAddedConnectionMessage(translate, action), [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_INTEGRATION]: (translate, action) => getRemovedConnectionMessage(translate, action), [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CARD_FEED]: (translate, action) => getAddedCardFeedMessage(translate, action), diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 022d6307c330..2e3be2cd2fba 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -764,6 +764,72 @@ type OriginalMessagePolicyChangeLog = { /** Whether the user joined the workspace via joining link */ didJoinPolicy?: boolean; + + /** Preformatted spend rule changelog line from the server */ + changeLogText?: string; + + /** Alternate preformatted spend rule changelog key (`text`) */ + text?: string; + + /** Alternate preformatted spend rule changelog key (`displayMessage`) */ + displayMessage?: string; + + /** Spend rule summary discriminator (`merchant`, `category`, `max_amount`, or `mix`) */ + summaryType?: string; + + /** Alias for summaryType */ + spendRuleSummaryType?: string; + + /** Spend rule effect (`block` or `allow`) */ + effect?: string; + + /** Alias for effect */ + ruleEffect?: string; + + /** Merchant name for merchant summary */ + merchant?: string; + + /** Category label for category summary */ + category?: string; + + /** Max amount comparison (`under` or `over`) */ + maxAmountComparison?: string; + + /** Human-readable max amount (e.g. "$1,000") */ + maxAmountDisplay?: string; + + /** Pre-built mix summary description */ + mixDescription?: string; + + /** Mix summary fragments joined with Oxford "and" */ + mixParts?: string[]; + + /** Single-card label (e.g. "Todd's Card") */ + cardName?: string; + + /** Number of cards when greater than 1 */ + cardCount?: number | string; + + /** Update kind (`mode` for block/allow toggle-only updates) */ + spendRuleChangeKind?: string; + + /** Alias for spendRuleChangeKind */ + changeType?: string; + + /** Previous spend rule mode for mode-only updates */ + fromSpendRuleMode?: string; + + /** Alias for fromSpendRuleMode */ + fromMode?: string; + + /** New spend rule mode for mode-only updates */ + toSpendRuleMode?: string; + + /** Alias for toSpendRuleMode */ + toMode?: string; + + /** Card last four when cardName is absent (alias) */ + lastFour?: string; }; /** Model of `join policy` report action */ From 9793421337880c1c3942bff7f3b2fbcf36c5b6f7 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 10:47:46 -0300 Subject: [PATCH 02/43] update add message --- src/languages/en.ts | 36 ++++++++++++ src/languages/es.ts | 36 ++++++++++++ src/libs/ReportActionsUtils.ts | 97 +++++++++++++++++++++++++++++-- src/types/onyx/OriginalMessage.ts | 79 +++++++------------------ 4 files changed, 185 insertions(+), 63 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index d1550f9a5098..d27c0a5ce050 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7273,6 +7273,42 @@ const translations = { updatedCardFeedLiability: (feedName: string, enabled: boolean) => `${enabled ? 'enabled' : 'disabled'} cardholders to delete card transactions for card feed "${feedName}"`, updatedCardFeedStatementPeriod: (feedName: string, newValue?: string, previousValue?: string) => `changed card feed "${feedName}" statement period end day${newValue ? ` to "${newValue}"` : ''}${previousValue ? ` (previously "${previousValue}")` : ''}`, + expensifyCardRule: { + actionVerb: { + block: 'blocked', + allow: 'allowed', + }, + amountOperator: { + over: 'over', + under: 'under', + }, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `amounts ${operator} ${amount}`, + theCard: 'the card', + namedCard: ({name}: {name: string}) => `'${name}'`, + multipleCards: ({count}: {count: number}) => `${count} cards`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} and ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')}, and ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${text === '' ? '' : ' '}on ${cards}`; + } + return text; + }, + }, preventSelfApproval: (oldValue: string, newValue: string) => `updated "Prevent self-approval" to "${newValue === 'true' ? 'Enabled' : 'Disabled'}" (previously "${oldValue === 'true' ? 'Enabled' : 'Disabled'}")`, updateMonthlyOffset: (oldValue: string, newValue: string) => { diff --git a/src/languages/es.ts b/src/languages/es.ts index 4d165fcfc90c..bb2976dedadb 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7126,6 +7126,42 @@ ${amount} para ${merchant} - ${date}`, `${enabled ? 'habilitó' : 'deshabilitó'} que los titulares de tarjetas eliminen transacciones de la fuente de tarjetas "${feedName}"`, updatedCardFeedStatementPeriod: (feedName: string, newValue?: string, previousValue?: string) => `cambió el día de cierre del período de estado de cuenta de la fuente de tarjetas "${feedName}"${newValue ? ` a "${newValue}"` : ''}${previousValue ? ` (previamente "${previousValue}")` : ''}`, + expensifyCardRule: { + actionVerb: { + block: 'bloqueó', + allow: 'permitió', + }, + amountOperator: { + over: 'mayores a', + under: 'menores a', + }, + amountFilter: ({operator, amount}) => `montos ${operator} ${amount}`, + theCard: 'la tarjeta', + namedCard: ({name}) => `'${name}'`, + multipleCards: ({count}) => `${count} tarjetas`, + joinFilters: ({items}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} y ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')} y ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${text === '' ? '' : ' '}en ${cards}`; + } + return text; + }, + }, preventSelfApproval: (oldValue, newValue) => `actualizó "Evitar la autoaprobación" a "${newValue === 'true' ? 'Habilitada' : 'Deshabilitada'}" (previamente "${oldValue === 'true' ? 'Habilitada' : 'Deshabilitada'}")`, setReceiptRequiredAmount: (newValue) => `estableció el importe requerido del recibo en "${newValue}"`, diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 7512059cd2e4..40c4eae4b2c0 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -15,6 +15,7 @@ import IntlStore from '@src/languages/IntlStore'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {isSpendRuleCategory} from '@src/types/form/SpendRuleForm'; import type { Card, CompanyCardFeed, @@ -3903,7 +3904,6 @@ function getUpdatedApprovalRuleMessage(translate: LocalizedTranslate, reportActi return getReportActionText(reportAction); } -/** Mirrors Web-Expensify `Report_Action_PolicyChangeLog_SpendRuleMessage::preformattedText` */ function getSpendRulePreformattedText(message: OriginalMessagePolicyChangeLog): string | undefined { for (const key of ['changeLogText', 'text', 'displayMessage'] as const) { const value = message?.[key]; @@ -4005,7 +4005,6 @@ function spendRuleFallbackAddOrUpdate(message: OriginalMessagePolicyChangeLog, i return `${verb} spend rule`; } -/** Mirrors Web-Expensify `Report_Action_PolicyChangeLog_SpendRuleMessage::buildAddOrUpdate` (structured path only; caller handles preformatted text). */ function spendRuleBuildAddOrUpdate(message: OriginalMessagePolicyChangeLog, isAdd: boolean): string { const effect = normalizeSpendRuleEffect(String(message?.effect ?? message?.ruleEffect ?? '')); if (effect === '') { @@ -4079,7 +4078,67 @@ function spendRuleBuildRemove(message: OriginalMessagePolicyChangeLog): string { return 'removed spend rule'; } -function getAddExpensifyCardRuleMessage(_translate: LocalizedTranslate, reportAction: OnyxEntry): string { +function spendRuleActionVerb(translate: LocalizedTranslate, action: string): string { + if (action === CONST.SPEND_RULES.ACTION.BLOCK) { + return translate('workspaceActions.expensifyCardRule.actionVerb.block'); + } + if (action === CONST.SPEND_RULES.ACTION.ALLOW) { + return translate('workspaceActions.expensifyCardRule.actionVerb.allow'); + } + return ''; +} + +function spendRuleAmountOperatorWord(translate: LocalizedTranslate, operator: string): string { + if (operator === 'lte') { + return translate('workspaceActions.expensifyCardRule.amountOperator.under'); + } + if (operator === 'gte') { + return translate('workspaceActions.expensifyCardRule.amountOperator.over'); + } + return ''; +} + +function spendRuleFormatAmountFilter(translate: LocalizedTranslate, amount: {operator?: unknown; value?: unknown}, currency: string): string { + const operator = typeof amount?.operator === 'string' ? amount.operator : ''; + const operatorWord = spendRuleAmountOperatorWord(translate, operator); + if (operatorWord === '') { + return ''; + } + const valueArray: unknown[] = Array.isArray(amount?.value) ? (amount.value as unknown[]) : []; + const firstValue = valueArray.at(0); + let cents = 0; + if (typeof firstValue === 'string' && firstValue !== '' && Number.isFinite(Number(firstValue))) { + cents = Number.parseInt(firstValue, 10); + } else if (typeof firstValue === 'number' && Number.isFinite(firstValue)) { + cents = firstValue; + } + return translate('workspaceActions.expensifyCardRule.amountFilter', {operator: operatorWord, amount: convertToShortDisplayString(cents, currency)}); +} + +function spendRuleCardsSummary(translate: LocalizedTranslate, cards: ReadonlyArray<{displayName?: string}> | undefined): string { + if (!cards || cards.length === 0) { + return translate('workspaceActions.expensifyCardRule.theCard'); + } + if (cards.length === 1) { + const displayName = cards.at(0)?.displayName ?? ''; + return displayName !== '' ? translate('workspaceActions.expensifyCardRule.namedCard', {name: displayName}) : translate('workspaceActions.expensifyCardRule.theCard'); + } + return translate('workspaceActions.expensifyCardRule.multipleCards', {count: cards.length}); +} + +function spendRuleJoinFilters(translate: LocalizedTranslate, items: readonly string[]): string { + const filtered = items.filter((value) => typeof value === 'string' && value !== ''); + return translate('workspaceActions.expensifyCardRule.joinFilters', {items: filtered}); +} + +function spendRuleCategoryDisplayName(translate: LocalizedTranslate, slug: string): string { + if (isSpendRuleCategory(slug)) { + return translate(`workspace.rules.spendRules.categoryOptions.${slug}`); + } + return slug; +} + +function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE)) { return ''; } @@ -4088,7 +4147,37 @@ function getAddExpensifyCardRuleMessage(_translate: LocalizedTranslate, reportAc if (pre) { return pre; } - return spendRuleBuildAddOrUpdate(message, true); + + const action = typeof message?.action === 'string' ? message.action : ''; + const currency = typeof message?.currency === 'string' && message.currency !== '' ? message.currency : CONST.CURRENCY.USD; + const merchants = (message?.merchants ?? []).filter((value) => typeof value === 'string' && value !== ''); + const categories = (message?.categories ?? []).filter((value) => typeof value === 'string' && value !== ''); + const amounts = message?.amounts ?? []; + const cards = message?.cards ?? []; + + const items: string[] = []; + for (const merchant of merchants) { + items.push(merchant); + } + for (const category of categories) { + items.push(spendRuleCategoryDisplayName(translate, category)); + } + for (const amount of amounts) { + const formatted = spendRuleFormatAmountFilter(translate, amount, currency); + if (formatted !== '') { + items.push(formatted); + } + } + + const verb = spendRuleActionVerb(translate, action); + const filtersDesc = spendRuleJoinFilters(translate, items); + const cardsSummary = spendRuleCardsSummary(translate, cards); + + if (verb === '' && filtersDesc === '' && cardsSummary === '') { + return spendRuleFallbackAddOrUpdate(message, true); + } + + return translate('workspaceActions.expensifyCardRule.addRule', {verb, filters: filtersDesc, cards: cardsSummary}); } function getUpdateExpensifyCardRuleMessage(_translate: LocalizedTranslate, reportAction: OnyxEntry): string { diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 7cff880513b0..6c5d456aec45 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -765,71 +765,32 @@ type OriginalMessagePolicyChangeLog = { /** Whether the user joined the workspace via joining link */ didJoinPolicy?: boolean; - /** Preformatted spend rule changelog line from the server */ - changeLogText?: string; + /** Spend rule action (`block` or `allow`) sent by the new structured changelog payload */ + action?: string; - /** Alternate preformatted spend rule changelog key (`text`) */ - text?: string; + /** Merchants included in a spend rule */ + merchants?: string[]; - /** Alternate preformatted spend rule changelog key (`displayMessage`) */ - displayMessage?: string; + /** Categories (slugs) included in a spend rule */ + categories?: string[]; - /** Spend rule summary discriminator (`merchant`, `category`, `max_amount`, or `mix`) */ - summaryType?: string; + /** Max-amount filters in a spend rule */ + amounts?: Array<{ + /** Operator (`gte` for "over", `lte` for "under") */ + operator: string; - /** Alias for summaryType */ - spendRuleSummaryType?: string; - - /** Spend rule effect (`block` or `allow`) */ - effect?: string; - - /** Alias for effect */ - ruleEffect?: string; - - /** Merchant name for merchant summary */ - merchant?: string; - - /** Category label for category summary */ - category?: string; - - /** Max amount comparison (`under` or `over`) */ - maxAmountComparison?: string; - - /** Human-readable max amount (e.g. "$1,000") */ - maxAmountDisplay?: string; - - /** Pre-built mix summary description */ - mixDescription?: string; - - /** Mix summary fragments joined with Oxford "and" */ - mixParts?: string[]; - - /** Single-card label (e.g. "Todd's Card") */ - cardName?: string; - - /** Number of cards when greater than 1 */ - cardCount?: number | string; - - /** Update kind (`mode` for block/allow toggle-only updates) */ - spendRuleChangeKind?: string; - - /** Alias for spendRuleChangeKind */ - changeType?: string; - - /** Previous spend rule mode for mode-only updates */ - fromSpendRuleMode?: string; - - /** Alias for fromSpendRuleMode */ - fromMode?: string; - - /** New spend rule mode for mode-only updates */ - toSpendRuleMode?: string; + /** Amount value as cents serialized to a string array (`['100000']`) */ + value: string[]; + }>; - /** Alias for toSpendRuleMode */ - toMode?: string; + /** Cards a spend rule is scoped to */ + cards?: Array<{ + /** Card identifier */ + cardID: number | string; - /** Card last four when cardName is absent (alias) */ - lastFour?: string; + /** Display name shown when the rule covers a single card */ + displayName?: string; + }>; }; /** Model of `join policy` report action */ From 63a150137413ca920b3dfc6e0700d8a09276f1e9 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 11:00:36 -0300 Subject: [PATCH 03/43] add remove case --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/ReportActionsUtils.ts | 188 +-------------------------------- 3 files changed, 7 insertions(+), 183 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index d27c0a5ce050..f641475bc8b9 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7308,6 +7308,7 @@ const translations = { } return text; }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `removed spend rule from ${cards}` : 'removed spend rule'), }, preventSelfApproval: (oldValue: string, newValue: string) => `updated "Prevent self-approval" to "${newValue === 'true' ? 'Enabled' : 'Disabled'}" (previously "${oldValue === 'true' ? 'Enabled' : 'Disabled'}")`, diff --git a/src/languages/es.ts b/src/languages/es.ts index bb2976dedadb..c0fd14d06aa3 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7161,6 +7161,7 @@ ${amount} para ${merchant} - ${date}`, } return text; }, + removeRule: ({cards}) => (cards !== '' ? `eliminó la regla de gasto de ${cards}` : 'eliminó la regla de gasto'), }, preventSelfApproval: (oldValue, newValue) => `actualizó "Evitar la autoaprobación" a "${newValue === 'true' ? 'Habilitada' : 'Deshabilitada'}" (previamente "${oldValue === 'true' ? 'Habilitada' : 'Deshabilitada'}")`, diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 40c4eae4b2c0..078f5bb92ef9 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3914,170 +3914,6 @@ function getSpendRulePreformattedText(message: OriginalMessagePolicyChangeLog): return undefined; } -function cardCountFromSpendRuleMessage(message: OriginalMessagePolicyChangeLog): number { - const raw = message?.cardCount; - if (typeof raw === 'number' && Number.isFinite(raw)) { - return raw; - } - if (typeof raw === 'string' && raw !== '' && /^\d+$/.test(raw)) { - return Number.parseInt(raw, 10); - } - return 0; -} - -function spendRuleCardScopeFragment(message: OriginalMessagePolicyChangeLog): string { - const cardCount = cardCountFromSpendRuleMessage(message); - const cardName = typeof message?.cardName === 'string' ? message.cardName : ''; - if (cardCount > 1) { - return `${cardCount} cards`; - } - if (cardName !== '') { - return cardName; - } - if (cardCount === 1) { - return '1 card'; - } - let lastFour = typeof message?.cardLastFour === 'string' ? message.cardLastFour : ''; - if (lastFour === '' && typeof message?.lastFour === 'string') { - lastFour = message.lastFour; - } - if (lastFour !== '') { - return `card ending in ${lastFour}`; - } - return ''; -} - -function normalizeSpendRuleEffect(raw: string): 'allow' | 'block' | '' { - const v = raw.toLowerCase(); - if (v === 'allow' || v === 'block') { - return v; - } - return ''; -} - -function normalizeSpendRuleModeLabel(raw: string): string { - const v = raw.toLowerCase(); - if (v === 'allow' || v === 'block') { - return v; - } - return ''; -} - -function normalizeSpendRuleMaxAmountComparison(raw: string): 'under' | 'over' | '' { - const v = raw.toLowerCase(); - if (v === 'under' || v === 'over') { - return v; - } - return ''; -} - -function mixDescriptionFromSpendRuleMessage(message: OriginalMessagePolicyChangeLog): string { - if (typeof message?.mixDescription === 'string' && message.mixDescription !== '') { - return message.mixDescription; - } - const mixParts = message?.mixParts; - if (!Array.isArray(mixParts)) { - return ''; - } - const parts: string[] = []; - for (const part of mixParts) { - if (typeof part === 'string' && part !== '') { - parts.push(part); - } - } - const n = parts.length; - if (n === 0) { - return ''; - } - if (n === 1) { - return parts.at(0) ?? ''; - } - const last = parts.pop(); - return `${parts.join(', ')}, and ${last}`; -} - -function spendRuleFallbackAddOrUpdate(message: OriginalMessagePolicyChangeLog, isAdd: boolean): string { - const verb = isAdd ? 'added' : 'updated'; - const onCard = spendRuleCardScopeFragment(message); - if (onCard !== '') { - return `${verb} spend rule on ${onCard}`; - } - return `${verb} spend rule`; -} - -function spendRuleBuildAddOrUpdate(message: OriginalMessagePolicyChangeLog, isAdd: boolean): string { - const effect = normalizeSpendRuleEffect(String(message?.effect ?? message?.ruleEffect ?? '')); - if (effect === '') { - return spendRuleFallbackAddOrUpdate(message, isAdd); - } - - const verb = effect === 'allow' ? 'allowed' : 'blocked'; - const summaryTypeRaw = message?.summaryType ?? message?.spendRuleSummaryType ?? ''; - const summaryType = typeof summaryTypeRaw === 'string' ? summaryTypeRaw : ''; - const onCard = spendRuleCardScopeFragment(message); - const onSuffix = onCard !== '' ? ` on ${onCard}` : ''; - - switch (summaryType) { - case 'merchant': { - const merchant = typeof message?.merchant === 'string' ? message.merchant : ''; - if (merchant === '') { - return spendRuleFallbackAddOrUpdate(message, isAdd); - } - return `${verb} ${merchant}${onSuffix}`; - } - case 'category': { - const category = typeof message?.category === 'string' ? message.category : ''; - if (category === '') { - return spendRuleFallbackAddOrUpdate(message, isAdd); - } - return `${verb} ${category}${onSuffix}`; - } - case 'max_amount': { - const comparison = normalizeSpendRuleMaxAmountComparison(typeof message?.maxAmountComparison === 'string' ? message.maxAmountComparison : ''); - const display = typeof message?.maxAmountDisplay === 'string' ? message.maxAmountDisplay : ''; - if (comparison === '' || display === '') { - return spendRuleFallbackAddOrUpdate(message, isAdd); - } - return `${verb} amounts ${comparison} ${display}${onSuffix}`; - } - case 'mix': { - const mix = mixDescriptionFromSpendRuleMessage(message); - if (mix === '') { - return spendRuleFallbackAddOrUpdate(message, isAdd); - } - return `${verb} ${mix.replace(/\.\s*$/, '')}.`; - } - default: - return spendRuleFallbackAddOrUpdate(message, isAdd); - } -} - -function isSpendRuleModeOnlyChange(message: OriginalMessagePolicyChangeLog): boolean { - const kind = message?.spendRuleChangeKind ?? message?.changeType ?? ''; - return typeof kind === 'string' && kind.toLowerCase() === 'mode'; -} - -function spendRuleBuildModeChange(message: OriginalMessagePolicyChangeLog): string { - const from = normalizeSpendRuleModeLabel(typeof message?.fromSpendRuleMode === 'string' ? message.fromSpendRuleMode : String(message?.fromMode ?? '')); - const to = normalizeSpendRuleModeLabel(typeof message?.toSpendRuleMode === 'string' ? message.toSpendRuleMode : String(message?.toMode ?? '')); - const onCard = spendRuleCardScopeFragment(message); - if (from !== '' && to !== '' && onCard !== '') { - return `changed spend rule from ${from} to ${to} on ${onCard}`; - } - if (from !== '' && to !== '') { - return `changed spend rule from ${from} to ${to}`; - } - return 'updated spend rule'; -} - -function spendRuleBuildRemove(message: OriginalMessagePolicyChangeLog): string { - const scope = spendRuleCardScopeFragment(message); - if (scope !== '') { - return `removed spend rule from ${scope}`; - } - return 'removed spend rule'; -} - function spendRuleActionVerb(translate: LocalizedTranslate, action: string): string { if (action === CONST.SPEND_RULES.ACTION.BLOCK) { return translate('workspaceActions.expensifyCardRule.actionVerb.block'); @@ -4174,28 +4010,13 @@ function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAct const cardsSummary = spendRuleCardsSummary(translate, cards); if (verb === '' && filtersDesc === '' && cardsSummary === '') { - return spendRuleFallbackAddOrUpdate(message, true); + return getReportActionText(reportAction); } return translate('workspaceActions.expensifyCardRule.addRule', {verb, filters: filtersDesc, cards: cardsSummary}); } -function getUpdateExpensifyCardRuleMessage(_translate: LocalizedTranslate, reportAction: OnyxEntry): string { - if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE)) { - return ''; - } - const message = getOriginalMessage(reportAction) ?? {}; - const pre = getSpendRulePreformattedText(message); - if (pre) { - return pre; - } - if (isSpendRuleModeOnlyChange(message)) { - return spendRuleBuildModeChange(message); - } - return spendRuleBuildAddOrUpdate(message, false); -} - -function getRemoveExpensifyCardRuleMessage(_translate: LocalizedTranslate, reportAction: OnyxEntry): string { +function getRemoveExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE)) { return ''; } @@ -4204,7 +4025,9 @@ function getRemoveExpensifyCardRuleMessage(_translate: LocalizedTranslate, repor if (pre) { return pre; } - return spendRuleBuildRemove(message); + const cards = message?.cards ?? []; + const cardsSummary = spendRuleCardsSummary(translate, cards); + return translate('workspaceActions.expensifyCardRule.removeRule', {cards: cardsSummary}); } function getRemovedFromApprovalChainMessage(translate: LocalizedTranslate, reportAction: OnyxEntry>) { @@ -4898,7 +4721,6 @@ export { getRemoveExpensifyCardRuleMessage, getSortedReportActions, getSortedReportActionsForDisplay, - getUpdateExpensifyCardRuleMessage, isCardBrokenConnectionAction, getCardConnectionBrokenMessage, getTextFromHtml, From fb62a4d8fab0d8388edd71441ba65eafa1922d11 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 11:13:00 -0300 Subject: [PATCH 04/43] add update case --- src/languages/en.ts | 29 +++ src/languages/es.ts | 29 +++ src/libs/ReportActionsUtils.ts | 333 +++++++++++++++++++++++++++++++++ 3 files changed, 391 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index f641475bc8b9..4d8e81915b28 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7309,6 +7309,35 @@ const translations = { return text; }, removeRule: ({cards}: {cards: string}) => (cards !== '' ? `removed spend rule from ${cards}` : 'removed spend rule'), + restrictionVerb: { + block: 'block', + allow: 'only allow', + }, + update: { + modeChange: ({fromMode, toMode, cards}: {fromMode: string; toMode: string; cards: string}) => + cards !== '' ? `changed spend rule from ${fromMode} to ${toMode} on ${cards}` : `changed spend rule from ${fromMode} to ${toMode}`, + appliedToAdditionalCards: ({count}: {count: number}) => `applied spend rule to ${count} additional cards`, + phraseVerb: { + added: 'added', + removed: 'removed', + changed: 'changed', + set: 'set', + applied: 'applied', + }, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} merchant '${value}'` : `merchant '${value}'`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} merchant from '${oldValue}' to '${newValue}'` : `merchant from '${oldValue}' to '${newValue}'`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} spend category '${value}'` : `spend category '${value}'`), + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} spend category from '${oldValue}' to '${newValue}'` : `spend category from '${oldValue}' to '${newValue}'`, + bodyMaxAmount: 'max amount', + bodyMaxAmountSet: ({value}: {value: string}) => `max amount to ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `max amount from ${oldValue} to ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `spend rule to ${count} additional cards`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `spend rule from ${cards}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} on ${cards}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} from ${cards}` : content), + }, }, preventSelfApproval: (oldValue: string, newValue: string) => `updated "Prevent self-approval" to "${newValue === 'true' ? 'Enabled' : 'Disabled'}" (previously "${oldValue === 'true' ? 'Enabled' : 'Disabled'}")`, diff --git a/src/languages/es.ts b/src/languages/es.ts index c0fd14d06aa3..850321339687 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7162,6 +7162,35 @@ ${amount} para ${merchant} - ${date}`, return text; }, removeRule: ({cards}) => (cards !== '' ? `eliminó la regla de gasto de ${cards}` : 'eliminó la regla de gasto'), + restrictionVerb: { + block: 'bloquear', + allow: 'solo permitir', + }, + update: { + modeChange: ({fromMode, toMode, cards}) => + cards !== '' ? `cambió la regla de gasto de ${fromMode} a ${toMode} en ${cards}` : `cambió la regla de gasto de ${fromMode} a ${toMode}`, + appliedToAdditionalCards: ({count}) => `aplicó la regla de gasto a ${count} tarjetas adicionales`, + phraseVerb: { + added: 'agregó', + removed: 'eliminó', + changed: 'cambió', + set: 'estableció', + applied: 'aplicó', + }, + bodyMerchant: ({adjective, value}) => (adjective !== '' ? `comerciante ${adjective} '${value}'` : `comerciante '${value}'`), + bodyMerchantChange: ({adjective, oldValue, newValue}) => + adjective !== '' ? `comerciante ${adjective} de '${oldValue}' a '${newValue}'` : `comerciante de '${oldValue}' a '${newValue}'`, + bodySpendCategory: ({adjective, value}) => (adjective !== '' ? `categoría de gasto ${adjective} '${value}'` : `categoría de gasto '${value}'`), + bodySpendCategoryChange: ({adjective, oldValue, newValue}) => + adjective !== '' ? `categoría de gasto ${adjective} de '${oldValue}' a '${newValue}'` : `categoría de gasto de '${oldValue}' a '${newValue}'`, + bodyMaxAmount: 'monto máximo', + bodyMaxAmountSet: ({value}) => `monto máximo en ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}) => `monto máximo de ${oldValue} a ${newValue}`, + bodyAppliedToAdditionalCards: ({count}) => `la regla de gasto a ${count} tarjetas adicionales`, + bodyRemovedFromCards: ({cards}) => `la regla de gasto de ${cards}`, + composeOnCards: ({content, cards}) => (cards !== '' ? `${content} en ${cards}` : content), + composeFromCards: ({content, cards}) => (cards !== '' ? `${content} de ${cards}` : content), + }, }, preventSelfApproval: (oldValue, newValue) => `actualizó "Evitar la autoaprobación" a "${newValue === 'true' ? 'Habilitada' : 'Deshabilitada'}" (previamente "${oldValue === 'true' ? 'Habilitada' : 'Deshabilitada'}")`, diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 078f5bb92ef9..77c03cbeff1b 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3974,6 +3974,179 @@ function spendRuleCategoryDisplayName(translate: LocalizedTranslate, slug: strin return slug; } +function spendRuleRestrictionVerb(translate: LocalizedTranslate, action: string): string { + if (action === CONST.SPEND_RULES.ACTION.BLOCK) { + return translate('workspaceActions.expensifyCardRule.restrictionVerb.block'); + } + if (action === CONST.SPEND_RULES.ACTION.ALLOW) { + return translate('workspaceActions.expensifyCardRule.restrictionVerb.allow'); + } + return action; +} + +function spendRuleAmountToCents(value: unknown): number { + const valueArray: unknown[] = Array.isArray(value) ? (value as unknown[]) : []; + const firstValue = valueArray.at(0); + if (typeof firstValue === 'string' && firstValue !== '' && Number.isFinite(Number(firstValue))) { + return Number.parseInt(firstValue, 10); + } + if (typeof firstValue === 'number' && Number.isFinite(firstValue)) { + return firstValue; + } + return 0; +} + +function spendRuleFormatAmountValue(amount: {value?: unknown}, currency: string): string { + return convertToShortDisplayString(spendRuleAmountToCents(amount?.value), currency); +} + +function spendRuleAmountKey(amount: {operator?: unknown; value?: unknown}): string { + const operator = typeof amount?.operator === 'string' ? amount.operator : ''; + return `${operator}:${spendRuleAmountToCents(amount?.value)}`; +} + +type SpendRuleStringDiff = {added: string[]; removed: string[]}; + +function spendRuleStringDiff(oldValues: readonly string[], newValues: readonly string[]): SpendRuleStringDiff { + const oldSet = Array.from(new Set(oldValues)); + const newSet = Array.from(new Set(newValues)); + const added = newSet.filter((value) => !oldSet.includes(value)).sort(); + const removed = oldSet.filter((value) => !newSet.includes(value)).sort(); + return {added, removed}; +} + +type SpendRuleAmount = {operator?: unknown; value?: unknown}; +type SpendRuleAmountDiff = {added: SpendRuleAmount[]; removed: SpendRuleAmount[]}; + +function spendRuleAmountDiff(oldAmounts: readonly SpendRuleAmount[], newAmounts: readonly SpendRuleAmount[]): SpendRuleAmountDiff { + const oldByKey = new Map(); + for (const amount of oldAmounts) { + oldByKey.set(spendRuleAmountKey(amount), amount); + } + const newByKey = new Map(); + for (const amount of newAmounts) { + newByKey.set(spendRuleAmountKey(amount), amount); + } + const added: SpendRuleAmount[] = []; + for (const [key, amount] of newByKey) { + if (!oldByKey.has(key)) { + added.push(amount); + } + } + const removed: SpendRuleAmount[] = []; + for (const [key, amount] of oldByKey) { + if (!newByKey.has(key)) { + removed.push(amount); + } + } + return {added, removed}; +} + +type SpendRuleCard = {cardID?: number | string; displayName?: string}; +type SpendRuleCardDiff = {added: SpendRuleCard[]; removed: SpendRuleCard[]}; + +function spendRuleCardID(card: SpendRuleCard): number | undefined { + const raw = card?.cardID; + if (typeof raw === 'number' && Number.isFinite(raw)) { + return raw; + } + if (typeof raw === 'string' && /^\d+$/.test(raw)) { + return Number.parseInt(raw, 10); + } + return undefined; +} + +function spendRuleCardDiff(oldCards: readonly SpendRuleCard[], newCards: readonly SpendRuleCard[]): SpendRuleCardDiff { + const oldByID = new Map(); + for (const card of oldCards) { + const id = spendRuleCardID(card); + if (id !== undefined) { + oldByID.set(id, card); + } + } + const newByID = new Map(); + for (const card of newCards) { + const id = spendRuleCardID(card); + if (id !== undefined) { + newByID.set(id, card); + } + } + const added: SpendRuleCard[] = []; + for (const [id, card] of newByID) { + if (!oldByID.has(id)) { + added.push(card); + } + } + const removed: SpendRuleCard[] = []; + for (const [id, card] of oldByID) { + if (!newByID.has(id)) { + removed.push(card); + } + } + return {added, removed}; +} + +type SpendRulePhraseVerb = 'added' | 'removed' | 'changed' | 'set' | 'applied'; +type SpendRulePhraseAdjective = '' | typeof CONST.SPEND_RULES.ACTION.BLOCK | typeof CONST.SPEND_RULES.ACTION.ALLOW; + +type SpendRulePhrase = { + verb: SpendRulePhraseVerb; + adjective: SpendRulePhraseAdjective; + bodyWithAdjective: string; + bodyWithoutAdjective: string; +}; + +function spendRulePhraseVerbWord(translate: LocalizedTranslate, verb: SpendRulePhraseVerb): string { + return translate(`workspaceActions.expensifyCardRule.update.phraseVerb.${verb}`); +} + +function spendRuleAdjectiveWord(translate: LocalizedTranslate, adjective: SpendRulePhraseAdjective): string { + if (adjective === CONST.SPEND_RULES.ACTION.BLOCK) { + return translate('workspaceActions.expensifyCardRule.actionVerb.block'); + } + if (adjective === CONST.SPEND_RULES.ACTION.ALLOW) { + return translate('workspaceActions.expensifyCardRule.actionVerb.allow'); + } + return ''; +} + +function joinSpendRulePhrases(translate: LocalizedTranslate, phrases: readonly SpendRulePhrase[]): string { + if (phrases.length === 0) { + return ''; + } + if (phrases.length === 1) { + const phrase = phrases.at(0); + if (!phrase) { + return ''; + } + return `${spendRulePhraseVerbWord(translate, phrase.verb)} ${phrase.bodyWithAdjective}`; + } + + const firstVerb = phrases.at(0)?.verb; + const allSameVerb = firstVerb !== undefined && phrases.every((phrase) => phrase.verb === firstVerb); + + if (!allSameVerb) { + const parts = phrases.map((phrase) => `${spendRulePhraseVerbWord(translate, phrase.verb)} ${phrase.bodyWithAdjective}`); + return spendRuleJoinFilters(translate, parts); + } + + const firstPhrase = phrases.at(0); + if (!firstPhrase) { + return ''; + } + const firstAdjective = firstPhrase.adjective; + const parts: string[] = [`${spendRulePhraseVerbWord(translate, firstPhrase.verb)} ${firstPhrase.bodyWithAdjective}`]; + for (let i = 1; i < phrases.length; i++) { + const phrase = phrases.at(i); + if (!phrase) { + continue; + } + const useOwnAdjective = phrase.adjective !== '' && phrase.adjective !== firstAdjective; + parts.push(useOwnAdjective ? phrase.bodyWithAdjective : phrase.bodyWithoutAdjective); + } + return spendRuleJoinFilters(translate, parts); +} + function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE)) { return ''; @@ -4016,6 +4189,165 @@ function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAct return translate('workspaceActions.expensifyCardRule.addRule', {verb, filters: filtersDesc, cards: cardsSummary}); } +function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { + if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE)) { + return ''; + } + const message = getOriginalMessage(reportAction) ?? {}; + const pre = getSpendRulePreformattedText(message); + if (pre) { + return pre; + } + + const oldAction = typeof message?.oldAction === 'string' ? message.oldAction : ''; + const newAction = typeof message?.action === 'string' ? message.action : ''; + const actionChanged = oldAction !== '' && oldAction !== newAction; + const currency = typeof message?.currency === 'string' && message.currency !== '' ? message.currency : CONST.CURRENCY.USD; + + const oldMerchants = (message?.oldMerchants ?? []).filter((value): value is string => typeof value === 'string' && value !== ''); + const newMerchants = (message?.merchants ?? []).filter((value): value is string => typeof value === 'string' && value !== ''); + const oldCategories = (message?.oldCategories ?? []).filter((value): value is string => typeof value === 'string' && value !== ''); + const newCategories = (message?.categories ?? []).filter((value): value is string => typeof value === 'string' && value !== ''); + const oldAmounts: SpendRuleAmount[] = (message?.oldAmounts ?? []).filter((amount): amount is SpendRuleAmount => typeof amount === 'object' && amount !== null); + const newAmounts: SpendRuleAmount[] = (message?.amounts ?? []).filter((amount): amount is SpendRuleAmount => typeof amount === 'object' && amount !== null); + const oldCards: SpendRuleCard[] = (message?.oldCards ?? []).filter((card): card is SpendRuleCard => typeof card === 'object' && card !== null); + const newCards: SpendRuleCard[] = (message?.cards ?? []).filter((card): card is SpendRuleCard => typeof card === 'object' && card !== null); + + const merchantDiff = spendRuleStringDiff(oldMerchants, newMerchants); + const categoryDiff = spendRuleStringDiff(oldCategories, newCategories); + const amountDiff = spendRuleAmountDiff(oldAmounts, newAmounts); + const cardDiff = spendRuleCardDiff(oldCards, newCards); + + const merchantsChanged = merchantDiff.added.length > 0 || merchantDiff.removed.length > 0; + const categoriesChanged = categoryDiff.added.length > 0 || categoryDiff.removed.length > 0; + const amountsChanged = amountDiff.added.length > 0 || amountDiff.removed.length > 0; + const cardsChanged = cardDiff.added.length > 0 || cardDiff.removed.length > 0; + const filtersAndCardsUnchanged = !merchantsChanged && !categoriesChanged && !amountsChanged && !cardsChanged; + + const newCardsSummary = spendRuleCardsSummary(translate, newCards); + + if (actionChanged && filtersAndCardsUnchanged) { + return translate('workspaceActions.expensifyCardRule.update.modeChange', { + fromMode: spendRuleRestrictionVerb(translate, oldAction), + toMode: spendRuleRestrictionVerb(translate, newAction), + cards: newCardsSummary, + }); + } + + if (cardsChanged && !merchantsChanged && !categoriesChanged && !amountsChanged && !actionChanged) { + if (cardDiff.added.length > 0 && cardDiff.removed.length === 0) { + return translate('workspaceActions.expensifyCardRule.update.appliedToAdditionalCards', {count: cardDiff.added.length}); + } + if (cardDiff.added.length === 0 && cardDiff.removed.length > 0) { + return translate('workspaceActions.expensifyCardRule.removeRule', {cards: spendRuleCardsSummary(translate, cardDiff.removed)}); + } + } + + const adjective: SpendRulePhraseAdjective = newAction === CONST.SPEND_RULES.ACTION.BLOCK || newAction === CONST.SPEND_RULES.ACTION.ALLOW ? newAction : ''; + const adjectiveWord = spendRuleAdjectiveWord(translate, adjective); + const phrases: SpendRulePhrase[] = []; + + if (merchantDiff.added.length === 1 && merchantDiff.removed.length === 1) { + const oldValue = merchantDiff.removed.at(0) ?? ''; + const newValue = merchantDiff.added.at(0) ?? ''; + phrases.push({ + verb: 'changed', + adjective, + bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchantChange', {adjective: adjectiveWord, oldValue, newValue}), + bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchantChange', {adjective: '', oldValue, newValue}), + }); + } else { + for (const merchant of merchantDiff.added) { + phrases.push({ + verb: 'added', + adjective, + bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchant', {adjective: adjectiveWord, value: merchant}), + bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchant', {adjective: '', value: merchant}), + }); + } + for (const merchant of merchantDiff.removed) { + phrases.push({ + verb: 'removed', + adjective, + bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchant', {adjective: adjectiveWord, value: merchant}), + bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchant', {adjective: '', value: merchant}), + }); + } + } + + if (categoryDiff.added.length === 1 && categoryDiff.removed.length === 1) { + const oldValue = spendRuleCategoryDisplayName(translate, categoryDiff.removed.at(0) ?? ''); + const newValue = spendRuleCategoryDisplayName(translate, categoryDiff.added.at(0) ?? ''); + phrases.push({ + verb: 'changed', + adjective, + bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategoryChange', {adjective: adjectiveWord, oldValue, newValue}), + bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategoryChange', {adjective: '', oldValue, newValue}), + }); + } else { + for (const category of categoryDiff.added) { + const value = spendRuleCategoryDisplayName(translate, category); + phrases.push({ + verb: 'added', + adjective, + bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', {adjective: adjectiveWord, value}), + bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', {adjective: '', value}), + }); + } + for (const category of categoryDiff.removed) { + const value = spendRuleCategoryDisplayName(translate, category); + phrases.push({ + verb: 'removed', + adjective, + bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', {adjective: adjectiveWord, value}), + bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', {adjective: '', value}), + }); + } + } + + if (amountDiff.added.length === 1 && amountDiff.removed.length === 1) { + const oldValue = spendRuleFormatAmountValue(amountDiff.removed.at(0) ?? {}, currency); + const newValue = spendRuleFormatAmountValue(amountDiff.added.at(0) ?? {}, currency); + const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountChange', {oldValue, newValue}); + phrases.push({verb: 'changed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); + } else { + for (const amount of amountDiff.added) { + const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountSet', {value: spendRuleFormatAmountValue(amount, currency)}); + phrases.push({verb: 'set', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); + } + for (let i = 0; i < amountDiff.removed.length; i++) { + const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmount'); + phrases.push({verb: 'removed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); + } + } + + if (cardDiff.added.length > 0) { + const body = translate('workspaceActions.expensifyCardRule.update.bodyAppliedToAdditionalCards', {count: cardDiff.added.length}); + phrases.push({verb: 'applied', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); + } + if (cardDiff.removed.length > 0) { + const body = translate('workspaceActions.expensifyCardRule.update.bodyRemovedFromCards', {cards: spendRuleCardsSummary(translate, cardDiff.removed)}); + phrases.push({verb: 'removed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); + } + + if (phrases.length === 0) { + return getAddExpensifyCardRuleMessage(translate, reportAction); + } + + const joined = joinSpendRulePhrases(translate, phrases); + + if (cardsChanged) { + return joined; + } + + const onlyRemovedPhrase = phrases.length === 1 && phrases.at(0)?.verb === 'removed'; + if (onlyRemovedPhrase) { + return translate('workspaceActions.expensifyCardRule.update.composeFromCards', {content: joined, cards: newCardsSummary}); + } + + return translate('workspaceActions.expensifyCardRule.update.composeOnCards', {content: joined, cards: newCardsSummary}); +} + function getRemoveExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE)) { return ''; @@ -4707,6 +5039,7 @@ export { getOneTransactionThreadReportID, getOriginalMessage, getAddExpensifyCardRuleMessage, + getUpdateExpensifyCardRuleMessage, getAddedApprovalRuleMessage, getDeletedApprovalRuleMessage, getUpdatedApprovalRuleMessage, From 8278d8fb38d0802528adb50b69a8dc10cee288ee Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 11:26:27 -0300 Subject: [PATCH 05/43] add types --- src/libs/ReportActionsUtils.ts | 33 +++++++++++-------------------- src/types/onyx/OriginalMessage.ts | 27 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 77c03cbeff1b..ecbe5e595b61 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -4100,16 +4100,6 @@ function spendRulePhraseVerbWord(translate: LocalizedTranslate, verb: SpendRuleP return translate(`workspaceActions.expensifyCardRule.update.phraseVerb.${verb}`); } -function spendRuleAdjectiveWord(translate: LocalizedTranslate, adjective: SpendRulePhraseAdjective): string { - if (adjective === CONST.SPEND_RULES.ACTION.BLOCK) { - return translate('workspaceActions.expensifyCardRule.actionVerb.block'); - } - if (adjective === CONST.SPEND_RULES.ACTION.ALLOW) { - return translate('workspaceActions.expensifyCardRule.actionVerb.allow'); - } - return ''; -} - function joinSpendRulePhrases(translate: LocalizedTranslate, phrases: readonly SpendRulePhrase[]): string { if (phrases.length === 0) { return ''; @@ -4204,14 +4194,14 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report const actionChanged = oldAction !== '' && oldAction !== newAction; const currency = typeof message?.currency === 'string' && message.currency !== '' ? message.currency : CONST.CURRENCY.USD; - const oldMerchants = (message?.oldMerchants ?? []).filter((value): value is string => typeof value === 'string' && value !== ''); - const newMerchants = (message?.merchants ?? []).filter((value): value is string => typeof value === 'string' && value !== ''); - const oldCategories = (message?.oldCategories ?? []).filter((value): value is string => typeof value === 'string' && value !== ''); - const newCategories = (message?.categories ?? []).filter((value): value is string => typeof value === 'string' && value !== ''); - const oldAmounts: SpendRuleAmount[] = (message?.oldAmounts ?? []).filter((amount): amount is SpendRuleAmount => typeof amount === 'object' && amount !== null); - const newAmounts: SpendRuleAmount[] = (message?.amounts ?? []).filter((amount): amount is SpendRuleAmount => typeof amount === 'object' && amount !== null); - const oldCards: SpendRuleCard[] = (message?.oldCards ?? []).filter((card): card is SpendRuleCard => typeof card === 'object' && card !== null); - const newCards: SpendRuleCard[] = (message?.cards ?? []).filter((card): card is SpendRuleCard => typeof card === 'object' && card !== null); + const oldMerchants = (message?.oldMerchants ?? []).filter((value) => value !== ''); + const newMerchants = (message?.merchants ?? []).filter((value) => value !== ''); + const oldCategories = (message?.oldCategories ?? []).filter((value) => value !== ''); + const newCategories = (message?.categories ?? []).filter((value) => value !== ''); + const oldAmounts = message?.oldAmounts ?? []; + const newAmounts = message?.amounts ?? []; + const oldCards = message?.oldCards ?? []; + const newCards = message?.cards ?? []; const merchantDiff = spendRuleStringDiff(oldMerchants, newMerchants); const categoryDiff = spendRuleStringDiff(oldCategories, newCategories); @@ -4244,7 +4234,7 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report } const adjective: SpendRulePhraseAdjective = newAction === CONST.SPEND_RULES.ACTION.BLOCK || newAction === CONST.SPEND_RULES.ACTION.ALLOW ? newAction : ''; - const adjectiveWord = spendRuleAdjectiveWord(translate, adjective); + const adjectiveWord = spendRuleActionVerb(translate, adjective); const phrases: SpendRulePhrase[] = []; if (merchantDiff.added.length === 1 && merchantDiff.removed.length === 1) { @@ -4315,9 +4305,10 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountSet', {value: spendRuleFormatAmountValue(amount, currency)}); phrases.push({verb: 'set', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); } - for (let i = 0; i < amountDiff.removed.length; i++) { + if (amountDiff.removed.length > 0) { const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmount'); - phrases.push({verb: 'removed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); + const removedPhrase: SpendRulePhrase = {verb: 'removed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}; + phrases.push(...Array.from({length: amountDiff.removed.length}).fill(removedPhrase)); } } diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 6c5d456aec45..9eccb2e23a9d 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -768,12 +768,21 @@ type OriginalMessagePolicyChangeLog = { /** Spend rule action (`block` or `allow`) sent by the new structured changelog payload */ action?: string; + /** Previous spend rule action when the rule's restriction type changed in an update */ + oldAction?: string; + /** Merchants included in a spend rule */ merchants?: string[]; + /** Previous list of merchants when a spend rule was updated */ + oldMerchants?: string[]; + /** Categories (slugs) included in a spend rule */ categories?: string[]; + /** Previous list of categories when a spend rule was updated */ + oldCategories?: string[]; + /** Max-amount filters in a spend rule */ amounts?: Array<{ /** Operator (`gte` for "over", `lte` for "under") */ @@ -783,6 +792,15 @@ type OriginalMessagePolicyChangeLog = { value: string[]; }>; + /** Previous list of max-amount filters when a spend rule was updated */ + oldAmounts?: Array<{ + /** Operator (`gte` for "over", `lte` for "under") */ + operator: string; + + /** Amount value as cents serialized to a string array (`['100000']`) */ + value: string[]; + }>; + /** Cards a spend rule is scoped to */ cards?: Array<{ /** Card identifier */ @@ -791,6 +809,15 @@ type OriginalMessagePolicyChangeLog = { /** Display name shown when the rule covers a single card */ displayName?: string; }>; + + /** Previous list of cards when a spend rule's card scope was updated */ + oldCards?: Array<{ + /** Card identifier */ + cardID: number | string; + + /** Display name shown when the rule covers a single card */ + displayName?: string; + }>; }; /** Model of `join policy` report action */ From 031af8fa3fa171e7873aa2dbeb4e6cc170f0b50e Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 13:02:07 -0300 Subject: [PATCH 06/43] rm pre --- src/libs/ReportActionsUtils.ts | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index ecbe5e595b61..1b2dbfe5bb97 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3904,16 +3904,6 @@ function getUpdatedApprovalRuleMessage(translate: LocalizedTranslate, reportActi return getReportActionText(reportAction); } -function getSpendRulePreformattedText(message: OriginalMessagePolicyChangeLog): string | undefined { - for (const key of ['changeLogText', 'text', 'displayMessage'] as const) { - const value = message?.[key]; - if (typeof value === 'string' && value !== '') { - return value; - } - } - return undefined; -} - function spendRuleActionVerb(translate: LocalizedTranslate, action: string): string { if (action === CONST.SPEND_RULES.ACTION.BLOCK) { return translate('workspaceActions.expensifyCardRule.actionVerb.block'); @@ -4142,11 +4132,6 @@ function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAct return ''; } const message = getOriginalMessage(reportAction) ?? {}; - const pre = getSpendRulePreformattedText(message); - if (pre) { - return pre; - } - const action = typeof message?.action === 'string' ? message.action : ''; const currency = typeof message?.currency === 'string' && message.currency !== '' ? message.currency : CONST.CURRENCY.USD; const merchants = (message?.merchants ?? []).filter((value) => typeof value === 'string' && value !== ''); @@ -4184,11 +4169,6 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report return ''; } const message = getOriginalMessage(reportAction) ?? {}; - const pre = getSpendRulePreformattedText(message); - if (pre) { - return pre; - } - const oldAction = typeof message?.oldAction === 'string' ? message.oldAction : ''; const newAction = typeof message?.action === 'string' ? message.action : ''; const actionChanged = oldAction !== '' && oldAction !== newAction; @@ -4344,10 +4324,6 @@ function getRemoveExpensifyCardRuleMessage(translate: LocalizedTranslate, report return ''; } const message = getOriginalMessage(reportAction) ?? {}; - const pre = getSpendRulePreformattedText(message); - if (pre) { - return pre; - } const cards = message?.cards ?? []; const cardsSummary = spendRuleCardsSummary(translate, cards); return translate('workspaceActions.expensifyCardRule.removeRule', {cards: cardsSummary}); From e6d23fdcc9257778343a48e2bc4f72cc85d40232 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 14:10:05 -0300 Subject: [PATCH 07/43] rename category function --- src/libs/ReportActionsUtils.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 1b2dbfe5bb97..bd798629464f 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3957,11 +3957,11 @@ function spendRuleJoinFilters(translate: LocalizedTranslate, items: readonly str return translate('workspaceActions.expensifyCardRule.joinFilters', {items: filtered}); } -function spendRuleCategoryDisplayName(translate: LocalizedTranslate, slug: string): string { - if (isSpendRuleCategory(slug)) { - return translate(`workspace.rules.spendRules.categoryOptions.${slug}`); +function getSpendRuleCategoryDisplayName(translate: LocalizedTranslate, category: string): string { + if (isSpendRuleCategory(category)) { + return translate(`workspace.rules.spendRules.categoryOptions.${category}`); } - return slug; + return category; } function spendRuleRestrictionVerb(translate: LocalizedTranslate, action: string): string { @@ -4132,19 +4132,19 @@ function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAct return ''; } const message = getOriginalMessage(reportAction) ?? {}; - const action = typeof message?.action === 'string' ? message.action : ''; - const currency = typeof message?.currency === 'string' && message.currency !== '' ? message.currency : CONST.CURRENCY.USD; - const merchants = (message?.merchants ?? []).filter((value) => typeof value === 'string' && value !== ''); - const categories = (message?.categories ?? []).filter((value) => typeof value === 'string' && value !== ''); - const amounts = message?.amounts ?? []; - const cards = message?.cards ?? []; + const action = message.action ?? CONST.SPEND_RULES.ACTION.ALLOW; + const currency = message.currency ?? CONST.CURRENCY.USD; + const merchants = message.merchants ?? []; + const categories = message.categories ?? []; + const amounts = message.amounts ?? []; + const cards = message.cards ?? []; const items: string[] = []; for (const merchant of merchants) { items.push(merchant); } for (const category of categories) { - items.push(spendRuleCategoryDisplayName(translate, category)); + items.push(getSpendRuleCategoryDisplayName(translate, category)); } for (const amount of amounts) { const formatted = spendRuleFormatAmountFilter(translate, amount, currency); @@ -4246,8 +4246,8 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report } if (categoryDiff.added.length === 1 && categoryDiff.removed.length === 1) { - const oldValue = spendRuleCategoryDisplayName(translate, categoryDiff.removed.at(0) ?? ''); - const newValue = spendRuleCategoryDisplayName(translate, categoryDiff.added.at(0) ?? ''); + const oldValue = getSpendRuleCategoryDisplayName(translate, categoryDiff.removed.at(0) ?? ''); + const newValue = getSpendRuleCategoryDisplayName(translate, categoryDiff.added.at(0) ?? ''); phrases.push({ verb: 'changed', adjective, @@ -4256,7 +4256,7 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report }); } else { for (const category of categoryDiff.added) { - const value = spendRuleCategoryDisplayName(translate, category); + const value = getSpendRuleCategoryDisplayName(translate, category); phrases.push({ verb: 'added', adjective, @@ -4265,7 +4265,7 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report }); } for (const category of categoryDiff.removed) { - const value = spendRuleCategoryDisplayName(translate, category); + const value = getSpendRuleCategoryDisplayName(translate, category); phrases.push({ verb: 'removed', adjective, From 418297f1d63f567a02ef49ca6b698788e36b480f Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 14:15:58 -0300 Subject: [PATCH 08/43] update remove case --- src/libs/ReportActionsUtils.ts | 60 +++++++++++++++------------------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index bd798629464f..1b5b577d8c21 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3904,7 +3904,7 @@ function getUpdatedApprovalRuleMessage(translate: LocalizedTranslate, reportActi return getReportActionText(reportAction); } -function spendRuleActionVerb(translate: LocalizedTranslate, action: string): string { +function getSpendRuleActionVerb(translate: LocalizedTranslate, action: string): string { if (action === CONST.SPEND_RULES.ACTION.BLOCK) { return translate('workspaceActions.expensifyCardRule.actionVerb.block'); } @@ -3915,33 +3915,25 @@ function spendRuleActionVerb(translate: LocalizedTranslate, action: string): str } function spendRuleAmountOperatorWord(translate: LocalizedTranslate, operator: string): string { - if (operator === 'lte') { + if (operator === CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN) { return translate('workspaceActions.expensifyCardRule.amountOperator.under'); } - if (operator === 'gte') { + if (operator === CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN) { return translate('workspaceActions.expensifyCardRule.amountOperator.over'); } return ''; } -function spendRuleFormatAmountFilter(translate: LocalizedTranslate, amount: {operator?: unknown; value?: unknown}, currency: string): string { - const operator = typeof amount?.operator === 'string' ? amount.operator : ''; - const operatorWord = spendRuleAmountOperatorWord(translate, operator); - if (operatorWord === '') { +function getSpendRuleAmountString(translate: LocalizedTranslate, amount: {operator: string; value: string[]}, currency: string): string { + const operatorWord = spendRuleAmountOperatorWord(translate, amount.operator); + const firstValue = amount.value.at(0); + if (firstValue === undefined) { return ''; } - const valueArray: unknown[] = Array.isArray(amount?.value) ? (amount.value as unknown[]) : []; - const firstValue = valueArray.at(0); - let cents = 0; - if (typeof firstValue === 'string' && firstValue !== '' && Number.isFinite(Number(firstValue))) { - cents = Number.parseInt(firstValue, 10); - } else if (typeof firstValue === 'number' && Number.isFinite(firstValue)) { - cents = firstValue; - } - return translate('workspaceActions.expensifyCardRule.amountFilter', {operator: operatorWord, amount: convertToShortDisplayString(cents, currency)}); + return translate('workspaceActions.expensifyCardRule.amountFilter', {operator: operatorWord, amount: convertAmountToDisplayString(Number(firstValue), currency)}); } -function spendRuleCardsSummary(translate: LocalizedTranslate, cards: ReadonlyArray<{displayName?: string}> | undefined): string { +function getSpendRuleCardsSummary(translate: LocalizedTranslate, cards: ReadonlyArray<{displayName?: string}> | undefined): string { if (!cards || cards.length === 0) { return translate('workspaceActions.expensifyCardRule.theCard'); } @@ -3952,7 +3944,7 @@ function spendRuleCardsSummary(translate: LocalizedTranslate, cards: ReadonlyArr return translate('workspaceActions.expensifyCardRule.multipleCards', {count: cards.length}); } -function spendRuleJoinFilters(translate: LocalizedTranslate, items: readonly string[]): string { +function getSpendRuleJoinFilters(translate: LocalizedTranslate, items: readonly string[]): string { const filtered = items.filter((value) => typeof value === 'string' && value !== ''); return translate('workspaceActions.expensifyCardRule.joinFilters', {items: filtered}); } @@ -4107,7 +4099,7 @@ function joinSpendRulePhrases(translate: LocalizedTranslate, phrases: readonly S if (!allSameVerb) { const parts = phrases.map((phrase) => `${spendRulePhraseVerbWord(translate, phrase.verb)} ${phrase.bodyWithAdjective}`); - return spendRuleJoinFilters(translate, parts); + return getSpendRuleJoinFilters(translate, parts); } const firstPhrase = phrases.at(0); @@ -4124,7 +4116,7 @@ function joinSpendRulePhrases(translate: LocalizedTranslate, phrases: readonly S const useOwnAdjective = phrase.adjective !== '' && phrase.adjective !== firstAdjective; parts.push(useOwnAdjective ? phrase.bodyWithAdjective : phrase.bodyWithoutAdjective); } - return spendRuleJoinFilters(translate, parts); + return getSpendRuleJoinFilters(translate, parts); } function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { @@ -4147,21 +4139,21 @@ function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAct items.push(getSpendRuleCategoryDisplayName(translate, category)); } for (const amount of amounts) { - const formatted = spendRuleFormatAmountFilter(translate, amount, currency); - if (formatted !== '') { - items.push(formatted); + const formattedAmount = getSpendRuleAmountString(translate, amount, currency); + if (formattedAmount !== '') { + items.push(formattedAmount); } } - const verb = spendRuleActionVerb(translate, action); - const filtersDesc = spendRuleJoinFilters(translate, items); - const cardsSummary = spendRuleCardsSummary(translate, cards); + const verb = getSpendRuleActionVerb(translate, action); + const filters = getSpendRuleJoinFilters(translate, items); + const cardsSummary = getSpendRuleCardsSummary(translate, cards); - if (verb === '' && filtersDesc === '' && cardsSummary === '') { + if (verb === '' && filters === '' && cardsSummary === '') { return getReportActionText(reportAction); } - return translate('workspaceActions.expensifyCardRule.addRule', {verb, filters: filtersDesc, cards: cardsSummary}); + return translate('workspaceActions.expensifyCardRule.addRule', {verb, filters, cards: cardsSummary}); } function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { @@ -4194,7 +4186,7 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report const cardsChanged = cardDiff.added.length > 0 || cardDiff.removed.length > 0; const filtersAndCardsUnchanged = !merchantsChanged && !categoriesChanged && !amountsChanged && !cardsChanged; - const newCardsSummary = spendRuleCardsSummary(translate, newCards); + const newCardsSummary = getSpendRuleCardsSummary(translate, newCards); if (actionChanged && filtersAndCardsUnchanged) { return translate('workspaceActions.expensifyCardRule.update.modeChange', { @@ -4209,12 +4201,12 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report return translate('workspaceActions.expensifyCardRule.update.appliedToAdditionalCards', {count: cardDiff.added.length}); } if (cardDiff.added.length === 0 && cardDiff.removed.length > 0) { - return translate('workspaceActions.expensifyCardRule.removeRule', {cards: spendRuleCardsSummary(translate, cardDiff.removed)}); + return translate('workspaceActions.expensifyCardRule.removeRule', {cards: getSpendRuleCardsSummary(translate, cardDiff.removed)}); } } const adjective: SpendRulePhraseAdjective = newAction === CONST.SPEND_RULES.ACTION.BLOCK || newAction === CONST.SPEND_RULES.ACTION.ALLOW ? newAction : ''; - const adjectiveWord = spendRuleActionVerb(translate, adjective); + const adjectiveWord = getSpendRuleActionVerb(translate, adjective); const phrases: SpendRulePhrase[] = []; if (merchantDiff.added.length === 1 && merchantDiff.removed.length === 1) { @@ -4297,7 +4289,7 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report phrases.push({verb: 'applied', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); } if (cardDiff.removed.length > 0) { - const body = translate('workspaceActions.expensifyCardRule.update.bodyRemovedFromCards', {cards: spendRuleCardsSummary(translate, cardDiff.removed)}); + const body = translate('workspaceActions.expensifyCardRule.update.bodyRemovedFromCards', {cards: getSpendRuleCardsSummary(translate, cardDiff.removed)}); phrases.push({verb: 'removed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); } @@ -4324,8 +4316,8 @@ function getRemoveExpensifyCardRuleMessage(translate: LocalizedTranslate, report return ''; } const message = getOriginalMessage(reportAction) ?? {}; - const cards = message?.cards ?? []; - const cardsSummary = spendRuleCardsSummary(translate, cards); + const cards = message.cards ?? []; + const cardsSummary = getSpendRuleCardsSummary(translate, cards); return translate('workspaceActions.expensifyCardRule.removeRule', {cards: cardsSummary}); } From a74f5fa4e8424a430b4cbbc889454cb39c586cbc Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 14:39:18 -0300 Subject: [PATCH 09/43] update vars --- src/languages/en.ts | 4 +- src/languages/es.ts | 4 +- src/libs/ReportActionsUtils.ts | 82 ++++++++++++++-------------------- 3 files changed, 38 insertions(+), 52 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 4d8e81915b28..e9a8f2b67a1c 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7314,8 +7314,8 @@ const translations = { allow: 'only allow', }, update: { - modeChange: ({fromMode, toMode, cards}: {fromMode: string; toMode: string; cards: string}) => - cards !== '' ? `changed spend rule from ${fromMode} to ${toMode} on ${cards}` : `changed spend rule from ${fromMode} to ${toMode}`, + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `changed spend rule from ${fromAction} to ${toAction} on ${cards}` : `changed spend rule from ${fromAction} to ${toAction}`, appliedToAdditionalCards: ({count}: {count: number}) => `applied spend rule to ${count} additional cards`, phraseVerb: { added: 'added', diff --git a/src/languages/es.ts b/src/languages/es.ts index 850321339687..f47a7330e81f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7167,8 +7167,8 @@ ${amount} para ${merchant} - ${date}`, allow: 'solo permitir', }, update: { - modeChange: ({fromMode, toMode, cards}) => - cards !== '' ? `cambió la regla de gasto de ${fromMode} a ${toMode} en ${cards}` : `cambió la regla de gasto de ${fromMode} a ${toMode}`, + modeChange: ({fromAction, toAction, cards}) => + cards !== '' ? `cambió la regla de gasto de ${fromAction} a ${toAction} en ${cards}` : `cambió la regla de gasto de ${fromAction} a ${toAction}`, appliedToAdditionalCards: ({count}) => `aplicó la regla de gasto a ${count} tarjetas adicionales`, phraseVerb: { added: 'agregó', diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 1b5b577d8c21..9907dda296f4 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3956,7 +3956,7 @@ function getSpendRuleCategoryDisplayName(translate: LocalizedTranslate, category return category; } -function spendRuleRestrictionVerb(translate: LocalizedTranslate, action: string): string { +function getSpendRuleRestrictionVerb(translate: LocalizedTranslate, action: string): string { if (action === CONST.SPEND_RULES.ACTION.BLOCK) { return translate('workspaceActions.expensifyCardRule.restrictionVerb.block'); } @@ -3982,14 +3982,9 @@ function spendRuleFormatAmountValue(amount: {value?: unknown}, currency: string) return convertToShortDisplayString(spendRuleAmountToCents(amount?.value), currency); } -function spendRuleAmountKey(amount: {operator?: unknown; value?: unknown}): string { - const operator = typeof amount?.operator === 'string' ? amount.operator : ''; - return `${operator}:${spendRuleAmountToCents(amount?.value)}`; -} - type SpendRuleStringDiff = {added: string[]; removed: string[]}; -function spendRuleStringDiff(oldValues: readonly string[], newValues: readonly string[]): SpendRuleStringDiff { +function computeSpendRuleStringDiff(oldValues: string[], newValues: string[]): SpendRuleStringDiff { const oldSet = Array.from(new Set(oldValues)); const newSet = Array.from(new Set(newValues)); const added = newSet.filter((value) => !oldSet.includes(value)).sort(); @@ -4000,28 +3995,19 @@ function spendRuleStringDiff(oldValues: readonly string[], newValues: readonly s type SpendRuleAmount = {operator?: unknown; value?: unknown}; type SpendRuleAmountDiff = {added: SpendRuleAmount[]; removed: SpendRuleAmount[]}; -function spendRuleAmountDiff(oldAmounts: readonly SpendRuleAmount[], newAmounts: readonly SpendRuleAmount[]): SpendRuleAmountDiff { - const oldByKey = new Map(); - for (const amount of oldAmounts) { - oldByKey.set(spendRuleAmountKey(amount), amount); - } - const newByKey = new Map(); - for (const amount of newAmounts) { - newByKey.set(spendRuleAmountKey(amount), amount); - } - const added: SpendRuleAmount[] = []; - for (const [key, amount] of newByKey) { - if (!oldByKey.has(key)) { - added.push(amount); - } - } - const removed: SpendRuleAmount[] = []; - for (const [key, amount] of oldByKey) { - if (!newByKey.has(key)) { - removed.push(amount); - } +function computeSpendRuleAmountDiff(oldAmounts: SpendRuleAmount[], newAmounts: SpendRuleAmount[]): SpendRuleAmountDiff { + const oldAmount = oldAmounts.at(0); + const newAmount = newAmounts.at(0); + const sameAmount = + oldAmount?.operator === newAmount?.operator && + spendRuleAmountToCents(oldAmount?.value) === spendRuleAmountToCents(newAmount?.value); + if (sameAmount) { + return {added: [], removed: []}; } - return {added, removed}; + return { + added: newAmount ? [newAmount] : [], + removed: oldAmount ? [oldAmount] : [], + }; } type SpendRuleCard = {cardID?: number | string; displayName?: string}; @@ -4038,7 +4024,7 @@ function spendRuleCardID(card: SpendRuleCard): number | undefined { return undefined; } -function spendRuleCardDiff(oldCards: readonly SpendRuleCard[], newCards: readonly SpendRuleCard[]): SpendRuleCardDiff { +function computeSpendRuleCardDiff(oldCards: SpendRuleCard[], newCards: SpendRuleCard[]): SpendRuleCardDiff { const oldByID = new Map(); for (const card of oldCards) { const id = spendRuleCardID(card); @@ -4161,24 +4147,24 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report return ''; } const message = getOriginalMessage(reportAction) ?? {}; - const oldAction = typeof message?.oldAction === 'string' ? message.oldAction : ''; - const newAction = typeof message?.action === 'string' ? message.action : ''; + const oldAction = message.oldAction ?? CONST.SPEND_RULES.ACTION.ALLOW; + const newAction = message.action ?? CONST.SPEND_RULES.ACTION.ALLOW; const actionChanged = oldAction !== '' && oldAction !== newAction; - const currency = typeof message?.currency === 'string' && message.currency !== '' ? message.currency : CONST.CURRENCY.USD; - - const oldMerchants = (message?.oldMerchants ?? []).filter((value) => value !== ''); - const newMerchants = (message?.merchants ?? []).filter((value) => value !== ''); - const oldCategories = (message?.oldCategories ?? []).filter((value) => value !== ''); - const newCategories = (message?.categories ?? []).filter((value) => value !== ''); - const oldAmounts = message?.oldAmounts ?? []; - const newAmounts = message?.amounts ?? []; - const oldCards = message?.oldCards ?? []; - const newCards = message?.cards ?? []; - - const merchantDiff = spendRuleStringDiff(oldMerchants, newMerchants); - const categoryDiff = spendRuleStringDiff(oldCategories, newCategories); - const amountDiff = spendRuleAmountDiff(oldAmounts, newAmounts); - const cardDiff = spendRuleCardDiff(oldCards, newCards); + const currency = message.currency ?? CONST.CURRENCY.USD; + + const oldMerchants = message.oldMerchants ?? []; + const newMerchants = message.merchants ?? []; + const oldCategories = message.oldCategories ?? []; + const newCategories = message.categories ?? []; + const oldAmounts = message.oldAmounts ?? []; + const newAmounts = message.amounts ?? []; + const oldCards = message.oldCards ?? []; + const newCards = message.cards ?? []; + + const merchantDiff = computeSpendRuleStringDiff(oldMerchants, newMerchants); + const categoryDiff = computeSpendRuleStringDiff(oldCategories, newCategories); + const amountDiff = computeSpendRuleAmountDiff(oldAmounts, newAmounts); + const cardDiff = computeSpendRuleCardDiff(oldCards, newCards); const merchantsChanged = merchantDiff.added.length > 0 || merchantDiff.removed.length > 0; const categoriesChanged = categoryDiff.added.length > 0 || categoryDiff.removed.length > 0; @@ -4190,8 +4176,8 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report if (actionChanged && filtersAndCardsUnchanged) { return translate('workspaceActions.expensifyCardRule.update.modeChange', { - fromMode: spendRuleRestrictionVerb(translate, oldAction), - toMode: spendRuleRestrictionVerb(translate, newAction), + fromAction: getSpendRuleRestrictionVerb(translate, oldAction), + toAction: getSpendRuleRestrictionVerb(translate, newAction), cards: newCardsSummary, }); } From 52bf786daf82cf05e04d5c29f55b61328591fbd0 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 14:39:50 -0300 Subject: [PATCH 10/43] fix prettier --- src/libs/ReportActionsUtils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 9907dda296f4..6e06f20eec27 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3998,9 +3998,7 @@ type SpendRuleAmountDiff = {added: SpendRuleAmount[]; removed: SpendRuleAmount[] function computeSpendRuleAmountDiff(oldAmounts: SpendRuleAmount[], newAmounts: SpendRuleAmount[]): SpendRuleAmountDiff { const oldAmount = oldAmounts.at(0); const newAmount = newAmounts.at(0); - const sameAmount = - oldAmount?.operator === newAmount?.operator && - spendRuleAmountToCents(oldAmount?.value) === spendRuleAmountToCents(newAmount?.value); + const sameAmount = oldAmount?.operator === newAmount?.operator && spendRuleAmountToCents(oldAmount?.value) === spendRuleAmountToCents(newAmount?.value); if (sameAmount) { return {added: [], removed: []}; } From 66d00a55230c5b25716a468d58a51b894fff11a2 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 14:47:56 -0300 Subject: [PATCH 11/43] rm unused import --- src/libs/ReportActionsUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 6e06f20eec27..ba8d373731a1 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -34,7 +34,6 @@ import type { OriginalMessageChangeLog, OriginalMessageExportIntegration, OriginalMessageMarkedReimbursed, - OriginalMessagePolicyChangeLog, OriginalMessageReimbursed, OriginalMessageUnreportedTransaction, PolicyBudgetFrequency, From 666d76a4c03ffc682fd2fafad604525984893682 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 15:15:34 -0300 Subject: [PATCH 12/43] fix operator --- src/libs/ReportActionsUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index ba8d373731a1..ff5389d4b94f 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3914,7 +3914,7 @@ function getSpendRuleActionVerb(translate: LocalizedTranslate, action: string): } function spendRuleAmountOperatorWord(translate: LocalizedTranslate, operator: string): string { - if (operator === CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN) { + if (operator === CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO) { return translate('workspaceActions.expensifyCardRule.amountOperator.under'); } if (operator === CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN) { From 56929197f878055219b78a3d9dfd3f7323a5b180 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 15:24:35 -0300 Subject: [PATCH 13/43] fix amounts --- src/libs/ReportActionsUtils.ts | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index ff5389d4b94f..80979487293a 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3965,20 +3965,16 @@ function getSpendRuleRestrictionVerb(translate: LocalizedTranslate, action: stri return action; } -function spendRuleAmountToCents(value: unknown): number { - const valueArray: unknown[] = Array.isArray(value) ? (value as unknown[]) : []; - const firstValue = valueArray.at(0); - if (typeof firstValue === 'string' && firstValue !== '' && Number.isFinite(Number(firstValue))) { - return Number.parseInt(firstValue, 10); +function spendRuleAmountToCents(value: string[]): number { + const firstValue = value.at(0) ?? ''; + if (firstValue === '' || !Number.isFinite(Number(firstValue))) { + return 0; } - if (typeof firstValue === 'number' && Number.isFinite(firstValue)) { - return firstValue; - } - return 0; + return Number.parseInt(firstValue, 10) * 100; } -function spendRuleFormatAmountValue(amount: {value?: unknown}, currency: string): string { - return convertToShortDisplayString(spendRuleAmountToCents(amount?.value), currency); +function spendRuleFormatAmountValue(amount: {value: string[]}, currency: string): string { + return convertAmountToDisplayString(spendRuleAmountToCents(amount.value), currency); } type SpendRuleStringDiff = {added: string[]; removed: string[]}; @@ -3991,13 +3987,16 @@ function computeSpendRuleStringDiff(oldValues: string[], newValues: string[]): S return {added, removed}; } -type SpendRuleAmount = {operator?: unknown; value?: unknown}; +type SpendRuleAmount = {operator: string; value: string[]}; type SpendRuleAmountDiff = {added: SpendRuleAmount[]; removed: SpendRuleAmount[]}; function computeSpendRuleAmountDiff(oldAmounts: SpendRuleAmount[], newAmounts: SpendRuleAmount[]): SpendRuleAmountDiff { const oldAmount = oldAmounts.at(0); const newAmount = newAmounts.at(0); - const sameAmount = oldAmount?.operator === newAmount?.operator && spendRuleAmountToCents(oldAmount?.value) === spendRuleAmountToCents(newAmount?.value); + if (!oldAmount || !newAmount) { + return {added: [], removed: []}; + } + const sameAmount = oldAmount.operator === newAmount.operator && spendRuleAmountToCents(oldAmount.value) === spendRuleAmountToCents(newAmount.value); if (sameAmount) { return {added: [], removed: []}; } From 8c02dd99391bf132f9d924fb23392fee111339aa Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 15:44:32 -0300 Subject: [PATCH 14/43] fix same amount case --- src/libs/ReportActionsUtils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 80979487293a..46ebd309b0de 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3965,7 +3965,7 @@ function getSpendRuleRestrictionVerb(translate: LocalizedTranslate, action: stri return action; } -function spendRuleAmountToCents(value: string[]): number { +function formatSpendRuleAmountToCents(value: string[]): number { const firstValue = value.at(0) ?? ''; if (firstValue === '' || !Number.isFinite(Number(firstValue))) { return 0; @@ -3974,7 +3974,7 @@ function spendRuleAmountToCents(value: string[]): number { } function spendRuleFormatAmountValue(amount: {value: string[]}, currency: string): string { - return convertAmountToDisplayString(spendRuleAmountToCents(amount.value), currency); + return convertAmountToDisplayString(formatSpendRuleAmountToCents(amount.value), currency); } type SpendRuleStringDiff = {added: string[]; removed: string[]}; @@ -3996,13 +3996,13 @@ function computeSpendRuleAmountDiff(oldAmounts: SpendRuleAmount[], newAmounts: S if (!oldAmount || !newAmount) { return {added: [], removed: []}; } - const sameAmount = oldAmount.operator === newAmount.operator && spendRuleAmountToCents(oldAmount.value) === spendRuleAmountToCents(newAmount.value); + const sameAmount = formatSpendRuleAmountToCents(oldAmount.value) === formatSpendRuleAmountToCents(newAmount.value); if (sameAmount) { return {added: [], removed: []}; } return { - added: newAmount ? [newAmount] : [], - removed: oldAmount ? [oldAmount] : [], + added: [newAmount], + removed: [oldAmount], }; } From ec0ff21f5a3cdf3c1de49c52557bc0decfe8eeed Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 16:30:20 -0300 Subject: [PATCH 15/43] add copy --- src/languages/de.ts | 51 ++++++++++++++++++++++++++++++++++++++++ src/languages/fr.ts | 51 ++++++++++++++++++++++++++++++++++++++++ src/languages/it.ts | 51 ++++++++++++++++++++++++++++++++++++++++ src/languages/ja.ts | 50 +++++++++++++++++++++++++++++++++++++++ src/languages/nl.ts | 51 ++++++++++++++++++++++++++++++++++++++++ src/languages/pl.ts | 51 ++++++++++++++++++++++++++++++++++++++++ src/languages/pt-BR.ts | 51 ++++++++++++++++++++++++++++++++++++++++ src/languages/zh-hans.ts | 50 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 406 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 4de93e2aae81..f04a1c096d67 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7575,6 +7575,57 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `${fieldType}-Berichtsfeld „${fieldName}“${defaultValue ? ` mit Standardwert „${defaultValue}“` : ''} hinzugefügt`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'aktiviert' : 'deaktiviert'} die Anforderung für Firmenkartenkäufe`, + expensifyCardRule: { + actionVerb: {block: 'blockiert', allow: 'erlaubt'}, + amountOperator: {over: 'über', under: 'unter'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `Beträge ${operator} ${amount}`, + theCard: 'die Karte', + multipleCards: ({count}: {count: number}) => `${count} Karten`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} und ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')} und ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${text === '' ? '' : ' '} auf ${cards}`; + } + return text; + }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `Ausgaberegel von ${cards} entfernt` : 'Ausgaberegel entfernt'), + restrictionVerb: {block: 'Block', allow: 'nur zulassen'}, + update: { + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `Ausgabenregel von ${fromAction} zu ${toAction} auf ${cards} geändert` : `Ausgabenregel von ${fromAction} in ${toAction} geändert`, + appliedToAdditionalCards: ({count}: {count: number}) => `Ausgaberegel auf ${count} zusätzliche Karten angewendet`, + phraseVerb: {added: 'hinzugefügt', removed: 'entfernt', changed: 'geändert', set: 'festlegen', applied: 'angewendet'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} Händler „${value}“` : `Händler*in „${value}“`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} Händler von „${oldValue}“ zu „${newValue}“` : `Händler*in von „${oldValue}“ zu „${newValue}“`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => + adjective !== '' ? `${adjective} Ausgabenkategorie „${value}“` : `Ausgabenkategorie „${value}“`, + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} Ausgabenkategorie von „${oldValue}“ zu „${newValue}“` : `Ausgabenkategorie von „${oldValue}“ auf „${newValue}“`, + bodyMaxAmount: 'Maximalbetrag', + bodyMaxAmountSet: ({value}: {value: string}) => `Maximalbetrag bis ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `Maximalbetrag von ${oldValue} auf ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `Ausgabenregel auf ${count} weitere Karten anwenden`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `Ausgabelimit von ${cards}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} auf ${cards}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} von ${cards}` : content), + }, + }, }, roomMembersPage: { memberNotFound: 'Mitglied nicht gefunden.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 580833a8046c..ba2d1f186201 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7597,6 +7597,57 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `a ajouté le champ de note de frais ${fieldType} « ${fieldName} »${defaultValue ? ` avec la valeur par défaut « ${defaultValue} »` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'activé' : 'désactivé'} l’exigence d’achats par carte d’entreprise`, + expensifyCardRule: { + actionVerb: {block: 'bloqué', allow: 'autorisé'}, + amountOperator: {over: 'terminé', under: 'sous'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `montants ${operator} ${amount}`, + theCard: 'la carte', + multipleCards: ({count}: {count: number}) => `${count} cartes`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} et ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')}, et ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${text === '' ? '' : ' '} sur ${cards}`; + } + return text; + }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `règle de dépense supprimée de ${cards}` : 'règle de dépense supprimée'), + restrictionVerb: {block: 'bloquer', allow: 'autoriser uniquement'}, + update: { + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `a modifié la règle de dépense de ${fromAction} à ${toAction} sur ${cards}` : `a modifié la règle de dépense de ${fromAction} à ${toAction}`, + appliedToAdditionalCards: ({count}: {count: number}) => `règle de dépense appliquée à ${count} cartes supplémentaires`, + phraseVerb: {added: 'ajouté', removed: 'supprimé', changed: 'modifié', set: 'définir', applied: 'appliqué'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `Commerçant·e ${adjective} « ${value} »` : `commerçant « ${value} »`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} commerçant de « ${oldValue} » à « ${newValue} »` : `commerçant de « ${oldValue} » à « ${newValue} »`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => + adjective !== '' ? `Catégorie de dépense ${adjective} « ${value} »` : `catégorie de dépense « ${value} »`, + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `Catégorie de dépense ${adjective} de « ${oldValue} » à « ${newValue} »` : `catégorie de dépense de « ${oldValue} » à « ${newValue} »`, + bodyMaxAmount: 'montant maximal', + bodyMaxAmountSet: ({value}: {value: string}) => `montant maximal à ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `montant maximum de ${oldValue} à ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `règle de dépense pour ${count} cartes supplémentaires`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `règle de dépense à partir de ${cards}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} sur ${cards}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} depuis ${cards}` : content), + }, + }, }, roomMembersPage: { memberNotFound: 'Membre introuvable.', diff --git a/src/languages/it.ts b/src/languages/it.ts index 85412c276f1f..0566007034bc 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7565,6 +7565,57 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `aggiunto campo di report ${fieldType} "${fieldName}"${defaultValue ? ` con valore predefinito "${defaultValue}"` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'abilitato' : 'disabilitato'} il requisito per gli acquisti con carta aziendale`, + expensifyCardRule: { + actionVerb: {block: 'bloccato', allow: 'consentito'}, + amountOperator: {over: 'terminato', under: 'sotto'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `importi ${operator} ${amount}`, + theCard: 'la carta', + multipleCards: ({count}: {count: number}) => `${count} carte`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} e ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')} e ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${text === '' ? '' : ' '} su ${cards}`; + } + return text; + }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `ha rimosso la regola di spesa da ${cards}` : 'regola di spesa rimossa'), + restrictionVerb: {block: 'bloc', allow: 'consenti solo'}, + update: { + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `ha modificato la regola di spesa da ${fromAction} a ${toAction} su ${cards}` : `ha modificato la regola di spesa da ${fromAction} a ${toAction}`, + appliedToAdditionalCards: ({count}: {count: number}) => `regola di spesa applicata a ${count} carte aggiuntive`, + phraseVerb: {added: 'aggiunto', removed: 'rimosso', changed: 'modificato', set: 'imposta', applied: 'applicato'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} esercente '${value}'` : `esercente '${value}'`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} esercente da '${oldValue}' a '${newValue}'` : `esercente da '${oldValue}' a '${newValue}'`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => + adjective !== '' ? `categoria di spesa ${adjective} "${value}"` : `categoria di spesa '${value}'`, + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `categoria di spesa ${adjective} da '${oldValue}' a '${newValue}'` : `categoria di spesa da '${oldValue}' a '${newValue}'`, + bodyMaxAmount: 'importo massimo', + bodyMaxAmountSet: ({value}: {value: string}) => `importo massimo pari a ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `importo massimo da ${oldValue} a ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regola di spesa per ${count} carte aggiuntive`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `regola di spesa da ${cards}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} su ${cards}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} da ${cards}` : content), + }, + }, }, roomMembersPage: { memberNotFound: 'Membro non trovato.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 35666c340a22..cd88a35a8a63 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7474,6 +7474,56 @@ ${reportName} addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `${fieldType}レポートフィールド「${fieldName}」を追加しました${defaultValue ? ` デフォルト値「${defaultValue}」付き` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? '有効' : '無効'} の法人カード購入要件`, + expensifyCardRule: { + actionVerb: {block: 'ブロック済み', allow: '許可済み'}, + amountOperator: {over: '終了', under: '以下の条件のもと'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `金額 ${operator} ${amount}`, + theCard: 'カード', + multipleCards: ({count}: {count: number}) => `${count} 件のカード`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} と ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')}、${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${cards}での${text === '' ? '' : ' '}`; + } + return text; + }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `${cards} から支出ルールを削除しました` : '支出ルールを削除しました'), + restrictionVerb: {block: 'ブロック', allow: 'のみ許可'}, + update: { + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `${cards} の支出ルールを ${fromAction} から ${toAction} に変更しました` : `支出ルールを${fromAction}から${toAction}に変更しました`, + appliedToAdditionalCards: ({count}: {count: number}) => `${count} 枚の追加カードに支出ルールを適用しました`, + phraseVerb: {added: '追加済み', removed: '削除済み', changed: '変更済み', set: '設定', applied: '適用済み'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective}なマーチャント「${value}」` : `加盟店「${value}」`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective}加盟店を「${oldValue}」から「${newValue}」に変更しました` : `加盟店名を「${oldValue}」から「${newValue}」に変更`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective}支出カテゴリー「${value}」` : `支出カテゴリ「${value}」`), + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective}支出カテゴリを「${oldValue}」から「${newValue}」に変更しました` : `支出カテゴリを「${oldValue}」から「${newValue}」に変更`, + bodyMaxAmount: '最大金額', + bodyMaxAmountSet: ({value}: {value: string}) => `最大金額を${value}に設定`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `最大金額を${oldValue}から${newValue}に変更`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `${count} 枚の追加カードに支出ルールを適用`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `${cards}の支出ルール`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${cards}の${content}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${cards} からの ${content}` : content), + }, + }, }, roomMembersPage: { memberNotFound: 'メンバーが見つかりません。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 11864eee56d8..67bd8a6ec2c1 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7539,6 +7539,57 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `heeft ${fieldType}-rapportveld "${fieldName}" toegevoegd${defaultValue ? ` met standaardwaarde "${defaultValue}"` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `vereiste ${enabled ? 'ingeschakeld' : 'uitgeschakeld'} voor bedrijfskaarttransacties`, + expensifyCardRule: { + actionVerb: {block: 'geblokkeerd', allow: 'toegestaan'}, + amountOperator: {over: 'over', under: 'onder'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `bedragen ${operator} ${amount}`, + theCard: 'de kaart', + multipleCards: ({count}: {count: number}) => `${count} kaarten`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} en ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')} en ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${text === '' ? '' : ' '}op ${cards}`; + } + return text; + }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `uitgavenregel verwijderd van ${cards}` : 'uitgave-regel verwijderd'), + restrictionVerb: {block: 'blokkeren', allow: 'alleen toestaan'}, + update: { + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `uitgave-regel gewijzigd van ${fromAction} naar ${toAction} op ${cards}` : `heeft bestedingsregel gewijzigd van ${fromAction} naar ${toAction}`, + appliedToAdditionalCards: ({count}: {count: number}) => `bestedingsregel toegepast op ${count} extra kaarten`, + phraseVerb: {added: 'toegevoegd', removed: 'verwijderd', changed: 'gewijzigd', set: 'instellen', applied: 'toegepast'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} handelaar '${value}'` : `handelaar '${value}'`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} handelaar van '${oldValue}' naar '${newValue}'` : `handelaar van '${oldValue}' naar '${newValue}'`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => + adjective !== '' ? `${adjective} uitgavencategorie '${value}'` : `uitgavencategorie '${value}'`, + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} uitgavencategorie van '${oldValue}' naar '${newValue}'` : `uitgavecategorie van '${oldValue}' naar '${newValue}'`, + bodyMaxAmount: 'max. bedrag', + bodyMaxAmountSet: ({value}: {value: string}) => `max. bedrag tot ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `max. bedrag van ${oldValue} naar ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `bestedsregel naar ${count} extra kaarten`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `bestedingsregel van ${cards}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} op ${cards}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} van ${cards}` : content), + }, + }, }, roomMembersPage: { memberNotFound: 'Lid niet gevonden.', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index fee79580a133..8442dae3cefc 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7530,6 +7530,57 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `dodano pole raportu typu ${fieldType} „${fieldName}”${defaultValue ? ` z domyślną wartością „${defaultValue}”` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'włączone' : 'wyłączone'} wymóg dotyczący zakupów kartą służbową`, + expensifyCardRule: { + actionVerb: {block: 'zablokowano', allow: 'dozwolone'}, + amountOperator: {over: 'ponad', under: 'pod'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `kwoty ${operator} ${amount}`, + theCard: 'karta', + multipleCards: ({count}: {count: number}) => `${count} karty`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} i ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')} i ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${text === '' ? '' : ' '} na ${cards}`; + } + return text; + }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `usunięto regułę wydatków z ${cards}` : 'usunięto regułę wydatków'), + restrictionVerb: {block: 'zablokuj', allow: 'zezwól tylko'}, + update: { + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `zmieniono regułę wydatków z ${fromAction} na ${toAction} na ${cards}` : `zmienił(a) regułę wydatków z ${fromAction} na ${toAction}`, + appliedToAdditionalCards: ({count}: {count: number}) => `zastosowano regułę wydatków do ${count} dodatkowych kart`, + phraseVerb: {added: 'dodano', removed: 'usunięto', changed: 'zmieniono', set: 'ustaw', applied: 'zastosowano'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} sprzedawca „${value}”` : `sprzedawca „${value}”`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} sprzedawcę z „${oldValue}” na „${newValue}”` : `sprzedawcę z „${oldValue}” na „${newValue}”`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => + adjective !== '' ? `kategoria wydatków ${adjective} „${value}”` : `kategoria wydatków „${value}”`, + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} kategorię wydatków z „${oldValue}” na „${newValue}”` : `kategoria wydatku z „${oldValue}” na „${newValue}”`, + bodyMaxAmount: 'maksymalna kwota', + bodyMaxAmountSet: ({value}: {value: string}) => `maksymalna kwota do ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `maksymalna kwota z ${oldValue} na ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `zasada wydatków dla ${count} dodatkowych kart`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `reguła wydatków z ${cards}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} na ${cards}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} z ${cards}` : content), + }, + }, }, roomMembersPage: { memberNotFound: 'Nie znaleziono członka.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index d44b4ef42fa6..c3b7bdfb51c2 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7531,6 +7531,57 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `adicionou o campo de relatório ${fieldType} "${fieldName}"${defaultValue ? ` com valor padrão "${defaultValue}"` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'ativado' : 'desativado'} o requisito de compras com cartão corporativo`, + expensifyCardRule: { + actionVerb: {block: 'bloqueado', allow: 'permitido'}, + amountOperator: {over: 'sobre', under: 'abaixo'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `valores ${operator} ${amount}`, + theCard: 'o cartão', + multipleCards: ({count}: {count: number}) => `${count} cartões`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} e ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')}, e ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${text === '' ? '' : ' '}em ${cards}`; + } + return text; + }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `removeu a regra de gasto de ${cards}` : 'removeu a regra de gasto'), + restrictionVerb: {block: 'bloquear', allow: 'permitir somente'}, + update: { + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `alterou a regra de gasto de ${fromAction} para ${toAction} em ${cards}` : `alterou a regra de gasto de ${fromAction} para ${toAction}`, + appliedToAdditionalCards: ({count}: {count: number}) => `regra de gasto aplicada a mais ${count} cartões`, + phraseVerb: {added: 'adicionado', removed: 'removido', changed: 'alterado', set: 'definir', applied: 'aplicado'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `Comerciante ${adjective} '${value}'` : `estabelecimento '${value}'`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} comerciante de '${oldValue}' para '${newValue}'` : `estabelecimento comercial de '${oldValue}' para '${newValue}'`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => + adjective !== '' ? `categoria de gasto ${adjective} '${value}'` : `categoria de despesa '${value}'`, + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `categoria de gasto ${adjective} de '${oldValue}' para '${newValue}'` : `categoria de gasto de '${oldValue}' para '${newValue}'`, + bodyMaxAmount: 'valor máximo', + bodyMaxAmountSet: ({value}: {value: string}) => `valor máximo de ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `valor máximo de ${oldValue} para ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regra de gasto para ${count} cartões adicionais`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `regra de gasto de ${cards}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} em ${cards}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} de ${cards}` : content), + }, + }, }, roomMembersPage: { memberNotFound: 'Membro não encontrado.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index d416e61161e1..4db9fce46503 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7344,6 +7344,56 @@ ${reportName} `已更改卡片流水“${feedName}”的账单周期截止日${newValue ? ` 为“${newValue}”` : ''}${previousValue ? ` (先前为“${previousValue}”)` : ''}`, addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `已添加 ${fieldType} 报告字段“${fieldName}”${defaultValue ? ` 默认值为“${defaultValue}”` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? '已启用' : '已禁用'} 公司商务卡消费要求`, + expensifyCardRule: { + actionVerb: {block: '已阻止', allow: '允许'}, + amountOperator: {over: '结束', under: '在…之下'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `金额 ${operator} ${amount}`, + theCard: '该卡', + multipleCards: ({count}: {count: number}) => `${count} 张卡片`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} 和 ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')},和 ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${cards} 上的 ${text === '' ? '' : ' '}`; + } + return text; + }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `已从${cards}中移除消费规则` : '已移除消费规则'), + restrictionVerb: {block: '封锁', allow: '仅允许'}, + update: { + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `已将 ${cards} 的消费规则从 ${fromAction} 更改为 ${toAction}` : `将消费规则从 ${fromAction} 更改为 ${toAction}`, + appliedToAdditionalCards: ({count}: {count: number}) => `已将消费规则应用到另外 ${count} 张卡片`, + phraseVerb: {added: '已添加', removed: '已移除', changed: '已更改', set: '设置', applied: '已应用'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} 商户“${value}”` : `商户“${value}”`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `将${adjective}商户从“${oldValue}”更改为“${newValue}”` : `商户从“${oldValue}”变更为“${newValue}”`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} 支出类别“${value}”` : `支出类别“${value}”`), + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `将${adjective}支出类别从“${oldValue}”更改为“${newValue}”` : `将支出类别从“${oldValue}”更改为“${newValue}”`, + bodyMaxAmount: '最高金额', + bodyMaxAmountSet: ({value}: {value: string}) => `最大金额为 ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `最大金额从 ${oldValue} 变更为 ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `将消费规则应用到另外 ${count} 张卡片`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `来自${cards}的支出规则`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${cards} 上的 ${content}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `来自${cards}的${content}` : content), + }, + }, }, roomMembersPage: { memberNotFound: '未找到成员。', From ac3d0d1f9e4ae64627e516c9dd8415c5a78f12af Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 16:36:49 -0300 Subject: [PATCH 16/43] fix types --- src/libs/ReportActionsUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 46ebd309b0de..a1cca596a264 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -4250,8 +4250,8 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report } if (amountDiff.added.length === 1 && amountDiff.removed.length === 1) { - const oldValue = spendRuleFormatAmountValue(amountDiff.removed.at(0) ?? {}, currency); - const newValue = spendRuleFormatAmountValue(amountDiff.added.at(0) ?? {}, currency); + const oldValue = spendRuleFormatAmountValue(amountDiff.removed.at(0) ?? {value: []}, currency); + const newValue = spendRuleFormatAmountValue(amountDiff.added.at(0) ?? {value: []}, currency); const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountChange', {oldValue, newValue}); phrases.push({verb: 'changed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); } else { From 3c53c0656b2a57a7616190438026091770808403 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 16:41:43 -0300 Subject: [PATCH 17/43] fix ts --- src/languages/en.ts | 1 - src/languages/es.ts | 1 - src/libs/ReportActionsUtils.ts | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index e9a8f2b67a1c..241973bacc65 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7284,7 +7284,6 @@ const translations = { }, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `amounts ${operator} ${amount}`, theCard: 'the card', - namedCard: ({name}: {name: string}) => `'${name}'`, multipleCards: ({count}: {count: number}) => `${count} cards`, joinFilters: ({items}: {items: string[]}) => { if (items.length === 0) { diff --git a/src/languages/es.ts b/src/languages/es.ts index f47a7330e81f..5aff1d827372 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7137,7 +7137,6 @@ ${amount} para ${merchant} - ${date}`, }, amountFilter: ({operator, amount}) => `montos ${operator} ${amount}`, theCard: 'la tarjeta', - namedCard: ({name}) => `'${name}'`, multipleCards: ({count}) => `${count} tarjetas`, joinFilters: ({items}) => { if (items.length === 0) { diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index a1cca596a264..267d85cdcc33 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3938,7 +3938,7 @@ function getSpendRuleCardsSummary(translate: LocalizedTranslate, cards: Readonly } if (cards.length === 1) { const displayName = cards.at(0)?.displayName ?? ''; - return displayName !== '' ? translate('workspaceActions.expensifyCardRule.namedCard', {name: displayName}) : translate('workspaceActions.expensifyCardRule.theCard'); + return displayName !== '' ? displayName : translate('workspaceActions.expensifyCardRule.theCard'); } return translate('workspaceActions.expensifyCardRule.multipleCards', {count: cards.length}); } From 6efc0867b996bd21df49450920b9aecda24938d6 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 22 May 2026 09:03:35 -0300 Subject: [PATCH 18/43] fix amount format --- src/libs/ReportActionsUtils.ts | 12 ++++++------ src/types/onyx/OriginalMessage.ts | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 45e278e17c6f..f26b35356cd4 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3979,7 +3979,7 @@ function formatSpendRuleAmountToCents(value: string[]): number { if (firstValue === '' || !Number.isFinite(Number(firstValue))) { return 0; } - return Number.parseInt(firstValue, 10) * 100; + return Math.round(parseFloat(firstValue) * 100); } function spendRuleFormatAmountValue(amount: {value: string[]}, currency: string): string { @@ -4019,12 +4019,12 @@ type SpendRuleCard = {cardID?: number | string; displayName?: string}; type SpendRuleCardDiff = {added: SpendRuleCard[]; removed: SpendRuleCard[]}; function spendRuleCardID(card: SpendRuleCard): number | undefined { - const raw = card?.cardID; - if (typeof raw === 'number' && Number.isFinite(raw)) { - return raw; + const cardID = card?.cardID; + if (typeof cardID === 'number' && Number.isFinite(cardID)) { + return cardID; } - if (typeof raw === 'string' && /^\d+$/.test(raw)) { - return Number.parseInt(raw, 10); + if (typeof cardID === 'string' && /^\d+$/.test(cardID)) { + return Number.parseInt(cardID, 10); } return undefined; } diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index b516a32249f4..58dbdff501e1 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -765,10 +765,10 @@ type OriginalMessagePolicyChangeLog = { /** Whether the user joined the workspace via joining link */ didJoinPolicy?: boolean; - /** Spend rule action (`block` or `allow`) sent by the new structured changelog payload */ + /** Spend rule action (`block` or `allow`) */ action?: string; - /** Previous spend rule action when the rule's restriction type changed in an update */ + /** Previous spend rule action when the rule's restriction type was updated */ oldAction?: string; /** Merchants included in a spend rule */ @@ -777,7 +777,7 @@ type OriginalMessagePolicyChangeLog = { /** Previous list of merchants when a spend rule was updated */ oldMerchants?: string[]; - /** Categories (slugs) included in a spend rule */ + /** Categories included in a spend rule */ categories?: string[]; /** Previous list of categories when a spend rule was updated */ @@ -785,19 +785,19 @@ type OriginalMessagePolicyChangeLog = { /** Max-amount filters in a spend rule */ amounts?: Array<{ - /** Operator (`gte` for "over", `lte` for "under") */ + /** Operator (`gt` for "over", `lte` for "under") */ operator: string; - /** Amount value as cents serialized to a string array (`['100000']`) */ + /** Amount value as a decimal dollar string array (e.g. `['100.40']`) */ value: string[]; }>; /** Previous list of max-amount filters when a spend rule was updated */ oldAmounts?: Array<{ - /** Operator (`gte` for "over", `lte` for "under") */ + /** Operator (`gt` for "over", `lte` for "under") */ operator: string; - /** Amount value as cents serialized to a string array (`['100000']`) */ + /** Amount value as a decimal dollar string array (e.g. `['100.40']`) */ value: string[]; }>; From 22995f3dea7d27c5e3ba19e0f9c7378f5d107d96 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 22 May 2026 09:20:11 -0300 Subject: [PATCH 19/43] update amount format --- src/libs/ReportActionsUtils.ts | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f26b35356cd4..74fd74db8ecb 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3974,16 +3974,10 @@ function getSpendRuleRestrictionVerb(translate: LocalizedTranslate, action: stri return action; } -function formatSpendRuleAmountToCents(value: string[]): number { - const firstValue = value.at(0) ?? ''; - if (firstValue === '' || !Number.isFinite(Number(firstValue))) { - return 0; - } - return Math.round(parseFloat(firstValue) * 100); -} - -function spendRuleFormatAmountValue(amount: {value: string[]}, currency: string): string { - return convertAmountToDisplayString(formatSpendRuleAmountToCents(amount.value), currency); +function formatSpendRuleAmount(amount: {value: string[]}, currency: string): string { + const firstValue = amount.value.at(0) ?? ''; + const amountInCents = Number.isFinite(Number(firstValue)) ? Math.round(parseFloat(firstValue) * 100) : 0; + return convertAmountToDisplayString(amountInCents, currency); } type SpendRuleStringDiff = {added: string[]; removed: string[]}; @@ -4005,7 +3999,7 @@ function computeSpendRuleAmountDiff(oldAmounts: SpendRuleAmount[], newAmounts: S if (!oldAmount || !newAmount) { return {added: [], removed: []}; } - const sameAmount = formatSpendRuleAmountToCents(oldAmount.value) === formatSpendRuleAmountToCents(newAmount.value); + const sameAmount = oldAmount.value === newAmount.value; if (sameAmount) { return {added: [], removed: []}; } @@ -4259,13 +4253,13 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report } if (amountDiff.added.length === 1 && amountDiff.removed.length === 1) { - const oldValue = spendRuleFormatAmountValue(amountDiff.removed.at(0) ?? {value: []}, currency); - const newValue = spendRuleFormatAmountValue(amountDiff.added.at(0) ?? {value: []}, currency); + const oldValue = formatSpendRuleAmount(amountDiff.removed.at(0) ?? {value: []}, currency); + const newValue = formatSpendRuleAmount(amountDiff.added.at(0) ?? {value: []}, currency); const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountChange', {oldValue, newValue}); phrases.push({verb: 'changed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); } else { for (const amount of amountDiff.added) { - const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountSet', {value: spendRuleFormatAmountValue(amount, currency)}); + const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountSet', {value: formatSpendRuleAmount(amount, currency)}); phrases.push({verb: 'set', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); } if (amountDiff.removed.length > 0) { From 14e26beb119a34303716db786b49c141bbfcbb9b Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 22 May 2026 09:30:48 -0300 Subject: [PATCH 20/43] fix bugs --- src/libs/ReportActionsUtils.ts | 30 ++++++++++++++++++++---------- src/types/onyx/OriginalMessage.ts | 11 +++++++++-- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 74fd74db8ecb..f326bf79506d 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -15,7 +15,6 @@ import IntlStore from '@src/languages/IntlStore'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; -import {isSpendRuleCategory} from '@src/types/form/SpendRuleForm'; import type { Card, CompanyCardFeed, @@ -3934,11 +3933,10 @@ function spendRuleAmountOperatorWord(translate: LocalizedTranslate, operator: st function getSpendRuleAmountString(translate: LocalizedTranslate, amount: {operator: string; value: string[]}, currency: string): string { const operatorWord = spendRuleAmountOperatorWord(translate, amount.operator); - const firstValue = amount.value.at(0); - if (firstValue === undefined) { + if (amount.value.length === 0) { return ''; } - return translate('workspaceActions.expensifyCardRule.amountFilter', {operator: operatorWord, amount: convertAmountToDisplayString(Number(firstValue), currency)}); + return translate('workspaceActions.expensifyCardRule.amountFilter', {operator: operatorWord, amount: formatSpendRuleAmount(amount, currency)}); } function getSpendRuleCardsSummary(translate: LocalizedTranslate, cards: ReadonlyArray<{displayName?: string}> | undefined): string { @@ -3958,8 +3956,9 @@ function getSpendRuleJoinFilters(translate: LocalizedTranslate, items: readonly } function getSpendRuleCategoryDisplayName(translate: LocalizedTranslate, category: string): string { - if (isSpendRuleCategory(category)) { - return translate(`workspace.rules.spendRules.categoryOptions.${category}`); + const knownCategories = Object.values(CONST.SPEND_RULES.CATEGORIES) as string[]; + if (knownCategories.includes(category)) { + return translate(`workspace.rules.spendRules.categoryOptions.${category as ValueOf}`); } return category; } @@ -3993,13 +3992,24 @@ function computeSpendRuleStringDiff(oldValues: string[], newValues: string[]): S type SpendRuleAmount = {operator: string; value: string[]}; type SpendRuleAmountDiff = {added: SpendRuleAmount[]; removed: SpendRuleAmount[]}; +function getSpendRuleValueInCents(value: string[]): number { + const firstValue = value.at(0) ?? ''; + return Number.isFinite(Number(firstValue)) ? Math.round(parseFloat(firstValue) * 100) : 0; +} + function computeSpendRuleAmountDiff(oldAmounts: SpendRuleAmount[], newAmounts: SpendRuleAmount[]): SpendRuleAmountDiff { const oldAmount = oldAmounts.at(0); const newAmount = newAmounts.at(0); - if (!oldAmount || !newAmount) { + if (!oldAmount && !newAmount) { return {added: [], removed: []}; } - const sameAmount = oldAmount.value === newAmount.value; + if (!oldAmount) { + return {added: newAmount ? [newAmount] : [], removed: []}; + } + if (!newAmount) { + return {added: [], removed: [oldAmount]}; + } + const sameAmount = getSpendRuleValueInCents(oldAmount.value) === getSpendRuleValueInCents(newAmount.value); if (sameAmount) { return {added: [], removed: []}; } @@ -4134,7 +4144,7 @@ function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAct const filters = getSpendRuleJoinFilters(translate, items); const cardsSummary = getSpendRuleCardsSummary(translate, cards); - if (verb === '' && filters === '' && cardsSummary === '') { + if (verb === '') { return getReportActionText(reportAction); } @@ -4279,7 +4289,7 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report } if (phrases.length === 0) { - return getAddExpensifyCardRuleMessage(translate, reportAction); + return getReportActionText(reportAction); } const joined = joinSpendRulePhrases(translate, phrases); diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 58dbdff501e1..68b81f6cadab 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -764,7 +764,10 @@ type OriginalMessagePolicyChangeLog = { /** Whether the user joined the workspace via joining link */ didJoinPolicy?: boolean; +}; +/** Model of an Expensify card spend rule change log action (add, update, or remove) */ +type OriginalMessageSpendRuleChangeLog = { /** Spend rule action (`block` or `allow`) */ action?: string; @@ -1625,8 +1628,11 @@ type OriginalMessageMap = { [CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENT_DIRECTOR_INFORMATION_REQUIRED]: OriginalMessageReimbursementDirectorInformationRequired; [CONST.REPORT.ACTIONS.TYPE.SETTLEMENT_ACCOUNT_LOCKED]: OriginalMessageSettlementAccountLocked; } & OldDotOriginalMessageMap & - Record, OriginalMessagePolicyChangeLog> & - Record, OriginalMessageChangeLog>; + Record, OriginalMessagePolicyChangeLog> & { + [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE]: OriginalMessageSpendRuleChangeLog; + [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE]: OriginalMessageSpendRuleChangeLog; + [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE]: OriginalMessageSpendRuleChangeLog; + } & Record, OriginalMessageChangeLog>; type OriginalMessage = T extends keyof OriginalMessageMap ? OriginalMessageMap[T] : never; @@ -1652,4 +1658,5 @@ export type { OriginalMessageMarkedReimbursed, OriginalMessageReimbursed, OriginalMessageSettlementAccountLocked, + OriginalMessageSpendRuleChangeLog, }; From bf9611039a4d222bc0d210ee3fc5bf8adbc8e278 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 22 May 2026 09:38:05 -0300 Subject: [PATCH 21/43] fix quote --- src/libs/ReportActionsUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f326bf79506d..4fb70bde5edc 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -4128,10 +4128,10 @@ function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAct const items: string[] = []; for (const merchant of merchants) { - items.push(merchant); + items.push(`'${merchant}'`); } for (const category of categories) { - items.push(getSpendRuleCategoryDisplayName(translate, category)); + items.push(`'${getSpendRuleCategoryDisplayName(translate, category)}'`); } for (const amount of amounts) { const formattedAmount = getSpendRuleAmountString(translate, amount, currency); From b7b6e8ac2738ba26e05508e985874e8a0dfeac31 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 22 May 2026 09:39:43 -0300 Subject: [PATCH 22/43] fix bugs --- src/languages/en.ts | 11 ++++------- src/libs/ReportActionsUtils.ts | 6 ++---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index afd92f23b9a7..272f22739859 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7495,21 +7495,18 @@ const translations = { addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { - text += `${text === '' ? '' : ' '}${filters}`; - } - if (cards !== '') { - text += `${text === '' ? '' : ' '}on ${cards}`; + text += ` ${filters}`; } + text += ` on ${cards}`; return text; }, - removeRule: ({cards}: {cards: string}) => (cards !== '' ? `removed spend rule from ${cards}` : 'removed spend rule'), + removeRule: ({cards}: {cards: string}) => `removed spend rule from ${cards}`, restrictionVerb: { block: 'block', allow: 'only allow', }, update: { - modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => - cards !== '' ? `changed spend rule from ${fromAction} to ${toAction} on ${cards}` : `changed spend rule from ${fromAction} to ${toAction}`, + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `changed spend rule from ${fromAction} to ${toAction} on ${cards}`, appliedToAdditionalCards: ({count}: {count: number}) => `applied spend rule to ${count} additional cards`, phraseVerb: { added: 'added', diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 4fb70bde5edc..06788296931c 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3974,9 +3974,7 @@ function getSpendRuleRestrictionVerb(translate: LocalizedTranslate, action: stri } function formatSpendRuleAmount(amount: {value: string[]}, currency: string): string { - const firstValue = amount.value.at(0) ?? ''; - const amountInCents = Number.isFinite(Number(firstValue)) ? Math.round(parseFloat(firstValue) * 100) : 0; - return convertAmountToDisplayString(amountInCents, currency); + return convertAmountToDisplayString(getSpendRuleValueInCents(amount.value), currency); } type SpendRuleStringDiff = {added: string[]; removed: string[]}; @@ -3994,7 +3992,7 @@ type SpendRuleAmountDiff = {added: SpendRuleAmount[]; removed: SpendRuleAmount[] function getSpendRuleValueInCents(value: string[]): number { const firstValue = value.at(0) ?? ''; - return Number.isFinite(Number(firstValue)) ? Math.round(parseFloat(firstValue) * 100) : 0; + return firstValue !== '' && Number.isFinite(Number(firstValue)) ? Math.round(parseFloat(firstValue) * 100) : 0; } function computeSpendRuleAmountDiff(oldAmounts: SpendRuleAmount[], newAmounts: SpendRuleAmount[]): SpendRuleAmountDiff { From b3f161e4263644749cd4aa7e06ff3b83f904f4ab Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 22 May 2026 09:41:45 -0300 Subject: [PATCH 23/43] fix card guard --- src/languages/en.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 272f22739859..6159289de040 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7526,8 +7526,8 @@ const translations = { bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `max amount from ${oldValue} to ${newValue}`, bodyAppliedToAdditionalCards: ({count}: {count: number}) => `spend rule to ${count} additional cards`, bodyRemovedFromCards: ({cards}: {cards: string}) => `spend rule from ${cards}`, - composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} on ${cards}` : content), - composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} from ${cards}` : content), + composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} on ${cards}`, + composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} from ${cards}`, }, }, preventSelfApproval: (oldValue: string, newValue: string) => From 51ed30a017ff083d3b455d21b0875ce35cc4ca5a Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 22 May 2026 09:48:30 -0300 Subject: [PATCH 24/43] apply copy change --- src/languages/de.ts | 27 +++++++-------- src/languages/es.ts | 71 +++++++++++++++------------------------- src/languages/fr.ts | 24 +++++++------- src/languages/it.ts | 26 +++++++-------- src/languages/ja.ts | 40 +++++++++++----------- src/languages/nl.ts | 28 ++++++++-------- src/languages/pl.ts | 22 ++++++------- src/languages/pt-BR.ts | 26 +++++++-------- src/languages/zh-hans.ts | 35 +++++++++----------- 9 files changed, 133 insertions(+), 166 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 59242842d364..ebc2957bf1b8 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7790,34 +7790,31 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { - text += `${text === '' ? '' : ' '}${filters}`; - } - if (cards !== '') { - text += `${text === '' ? '' : ' '} auf ${cards}`; + text += ` ${filters}`; } + text += `auf ${cards}`; return text; }, - removeRule: ({cards}: {cards: string}) => (cards !== '' ? `Ausgaberegel von ${cards} entfernt` : 'Ausgaberegel entfernt'), + removeRule: ({cards}: {cards: string}) => `Ausgaberegel von ${cards} entfernt`, restrictionVerb: {block: 'Block', allow: 'nur zulassen'}, update: { - modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => - cards !== '' ? `Ausgabenregel von ${fromAction} zu ${toAction} auf ${cards} geändert` : `Ausgabenregel von ${fromAction} in ${toAction} geändert`, - appliedToAdditionalCards: ({count}: {count: number}) => `Ausgaberegel auf ${count} zusätzliche Karten angewendet`, + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `Ausgaberegel von ${fromAction} zu ${toAction} auf ${cards} geändert`, + appliedToAdditionalCards: ({count}: {count: number}) => `Ausgaberegel auf ${count} weitere Karten angewendet`, phraseVerb: {added: 'hinzugefügt', removed: 'entfernt', changed: 'geändert', set: 'festlegen', applied: 'angewendet'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} Händler „${value}“` : `Händler*in „${value}“`), + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} Händler „${value}“` : `Händler\\*in „${value}“`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} Händler von „${oldValue}“ zu „${newValue}“` : `Händler*in von „${oldValue}“ zu „${newValue}“`, + adjective !== '' ? `${adjective} Händler von „${oldValue}“ zu „${newValue}“` : `Händler von „${oldValue}“ zu „${newValue}“`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => adjective !== '' ? `${adjective} Ausgabenkategorie „${value}“` : `Ausgabenkategorie „${value}“`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} Ausgabenkategorie von „${oldValue}“ zu „${newValue}“` : `Ausgabenkategorie von „${oldValue}“ auf „${newValue}“`, + adjective !== '' ? `${adjective} Ausgabenkategorie von „${oldValue}“ zu „${newValue}“` : `Ausgabenkategorie von „${oldValue}“ zu „${newValue}“`, bodyMaxAmount: 'Maximalbetrag', bodyMaxAmountSet: ({value}: {value: string}) => `Maximalbetrag bis ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `Maximalbetrag von ${oldValue} auf ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `Ausgabenregel auf ${count} weitere Karten anwenden`, - bodyRemovedFromCards: ({cards}: {cards: string}) => `Ausgabelimit von ${cards}`, - composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} auf ${cards}` : content), - composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} von ${cards}` : content), + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `Ausgabenregel auf ${count} zusätzliche Karten`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `Ausgaberegel von ${cards}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} auf ${cards}`, + composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} von ${cards}`, }, }, }, diff --git a/src/languages/es.ts b/src/languages/es.ts index 7fb530da2b7a..c2c7baff5e1f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7263,18 +7263,12 @@ ${amount} para ${merchant} - ${date}`, updatedCardFeedStatementPeriod: (feedName: string, newValue?: string, previousValue?: string) => `cambió el día de cierre del período de estado de cuenta de la fuente de tarjetas "${feedName}"${newValue ? ` a "${newValue}"` : ''}${previousValue ? ` (previamente "${previousValue}")` : ''}`, expensifyCardRule: { - actionVerb: { - block: 'bloqueó', - allow: 'permitió', - }, - amountOperator: { - over: 'mayores a', - under: 'menores a', - }, - amountFilter: ({operator, amount}) => `montos ${operator} ${amount}`, + actionVerb: {block: 'bloqueado', allow: 'permitido'}, + amountOperator: {over: 'encima', under: 'debajo'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `cantidades ${operator} ${amount}`, theCard: 'la tarjeta', - multipleCards: ({count}) => `${count} tarjetas`, - joinFilters: ({items}) => { + multipleCards: ({count}: {count: number}) => `${count} tarjetas`, + joinFilters: ({items}: {items: string[]}) => { if (items.length === 0) { return ''; } @@ -7286,45 +7280,34 @@ ${amount} para ${merchant} - ${date}`, } return `${items.slice(0, -1).join(', ')} y ${items.at(-1)}`; }, - addRule: ({verb, filters, cards}) => { + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { - text += `${text === '' ? '' : ' '}${filters}`; - } - if (cards !== '') { - text += `${text === '' ? '' : ' '}en ${cards}`; + text += ` ${filters}`; } + text += `en ${cards}`; return text; }, - removeRule: ({cards}) => (cards !== '' ? `eliminó la regla de gasto de ${cards}` : 'eliminó la regla de gasto'), - restrictionVerb: { - block: 'bloquear', - allow: 'solo permitir', - }, + removeRule: ({cards}: {cards: string}) => `eliminó la regla de gasto de ${cards}`, + restrictionVerb: {block: 'bloquear', allow: 'permitir solo'}, update: { - modeChange: ({fromAction, toAction, cards}) => - cards !== '' ? `cambió la regla de gasto de ${fromAction} a ${toAction} en ${cards}` : `cambió la regla de gasto de ${fromAction} a ${toAction}`, - appliedToAdditionalCards: ({count}) => `aplicó la regla de gasto a ${count} tarjetas adicionales`, - phraseVerb: { - added: 'agregó', - removed: 'eliminó', - changed: 'cambió', - set: 'estableció', - applied: 'aplicó', - }, - bodyMerchant: ({adjective, value}) => (adjective !== '' ? `comerciante ${adjective} '${value}'` : `comerciante '${value}'`), - bodyMerchantChange: ({adjective, oldValue, newValue}) => - adjective !== '' ? `comerciante ${adjective} de '${oldValue}' a '${newValue}'` : `comerciante de '${oldValue}' a '${newValue}'`, - bodySpendCategory: ({adjective, value}) => (adjective !== '' ? `categoría de gasto ${adjective} '${value}'` : `categoría de gasto '${value}'`), - bodySpendCategoryChange: ({adjective, oldValue, newValue}) => - adjective !== '' ? `categoría de gasto ${adjective} de '${oldValue}' a '${newValue}'` : `categoría de gasto de '${oldValue}' a '${newValue}'`, - bodyMaxAmount: 'monto máximo', - bodyMaxAmountSet: ({value}) => `monto máximo en ${value}`, - bodyMaxAmountChange: ({oldValue, newValue}) => `monto máximo de ${oldValue} a ${newValue}`, - bodyAppliedToAdditionalCards: ({count}) => `la regla de gasto a ${count} tarjetas adicionales`, - bodyRemovedFromCards: ({cards}) => `la regla de gasto de ${cards}`, - composeOnCards: ({content, cards}) => (cards !== '' ? `${content} en ${cards}` : content), - composeFromCards: ({content, cards}) => (cards !== '' ? `${content} de ${cards}` : content), + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `cambió la regla de gasto de ${fromAction} a ${toAction} en ${cards}`, + appliedToAdditionalCards: ({count}: {count: number}) => `aplicó la regla de gasto a ${count} tarjetas adicionales`, + phraseVerb: {added: 'añadido', removed: 'eliminado', changed: 'cambiado', set: 'establecer', applied: 'aplicado'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `Comercio ${adjective} «${value}»` : `comercio «${value}»`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} comercio de '${oldValue}' a '${newValue}'` : `comercio de «${oldValue}» a «${newValue}»`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => + adjective !== '' ? `Categoría de gasto ${adjective} «${value}»` : `categoría de gasto «${value}»`, + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `Categoría de gasto ${adjective} de '${oldValue}' a '${newValue}'` : `categoría de gasto de «${oldValue}» a «${newValue}»`, + bodyMaxAmount: 'importe máximo', + bodyMaxAmountSet: ({value}: {value: string}) => `importe máximo hasta ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `importe máximo de ${oldValue} a ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regla de gasto para ${count} tarjetas adicionales`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `regla de gasto de ${cards}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} en ${cards}`, + composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} de ${cards}`, }, }, preventSelfApproval: (oldValue, newValue) => diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 41bc1daa4d2f..16d3d9269dcb 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7814,39 +7814,37 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e if (items.length === 2) { return `${items.at(0)} et ${items.at(1)}`; } - return `${items.slice(0, -1).join(', ')}, et ${items.at(-1)}`; + return `${items.slice(0, -1).join(', ')} et ${items.at(-1)}`; }, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { - text += `${text === '' ? '' : ' '}${filters}`; - } - if (cards !== '') { - text += `${text === '' ? '' : ' '} sur ${cards}`; + text += ` ${filters}`; } + text += `sur ${cards}`; return text; }, - removeRule: ({cards}: {cards: string}) => (cards !== '' ? `règle de dépense supprimée de ${cards}` : 'règle de dépense supprimée'), + removeRule: ({cards}: {cards: string}) => `a supprimé la règle de dépense de ${cards}`, restrictionVerb: {block: 'bloquer', allow: 'autoriser uniquement'}, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => - cards !== '' ? `a modifié la règle de dépense de ${fromAction} à ${toAction} sur ${cards}` : `a modifié la règle de dépense de ${fromAction} à ${toAction}`, + `a modifié la règle de dépense de ${fromAction} à ${toAction} sur ${cards}`, appliedToAdditionalCards: ({count}: {count: number}) => `règle de dépense appliquée à ${count} cartes supplémentaires`, phraseVerb: {added: 'ajouté', removed: 'supprimé', changed: 'modifié', set: 'définir', applied: 'appliqué'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `Commerçant·e ${adjective} « ${value} »` : `commerçant « ${value} »`), + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `Commerçant ${adjective} « ${value} »` : `commerçant « ${value} »`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} commerçant de « ${oldValue} » à « ${newValue} »` : `commerçant de « ${oldValue} » à « ${newValue} »`, + adjective !== '' ? `Commerçant ${adjective} de « ${oldValue} » à « ${newValue} »` : `commerçant de « ${oldValue} » à « ${newValue} »`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => adjective !== '' ? `Catégorie de dépense ${adjective} « ${value} »` : `catégorie de dépense « ${value} »`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `Catégorie de dépense ${adjective} de « ${oldValue} » à « ${newValue} »` : `catégorie de dépense de « ${oldValue} » à « ${newValue} »`, bodyMaxAmount: 'montant maximal', - bodyMaxAmountSet: ({value}: {value: string}) => `montant maximal à ${value}`, - bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `montant maximum de ${oldValue} à ${newValue}`, + bodyMaxAmountSet: ({value}: {value: string}) => `montant maximum à ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `montant maximal de ${oldValue} à ${newValue}`, bodyAppliedToAdditionalCards: ({count}: {count: number}) => `règle de dépense pour ${count} cartes supplémentaires`, bodyRemovedFromCards: ({cards}: {cards: string}) => `règle de dépense à partir de ${cards}`, - composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} sur ${cards}` : content), - composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} depuis ${cards}` : content), + composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} sur ${cards}`, + composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} depuis ${cards}`, }, }, }, diff --git a/src/languages/it.ts b/src/languages/it.ts index 6ba66a7cd730..06eb393489ab 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7781,34 +7781,32 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { - text += `${text === '' ? '' : ' '}${filters}`; - } - if (cards !== '') { - text += `${text === '' ? '' : ' '} su ${cards}`; + text += ` ${filters}`; } + text += `su ${cards}`; return text; }, - removeRule: ({cards}: {cards: string}) => (cards !== '' ? `ha rimosso la regola di spesa da ${cards}` : 'regola di spesa rimossa'), - restrictionVerb: {block: 'bloc', allow: 'consenti solo'}, + removeRule: ({cards}: {cards: string}) => `ha rimosso la regola di spesa da ${cards}`, + restrictionVerb: {block: 'blocca', allow: 'consenti solo'}, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => - cards !== '' ? `ha modificato la regola di spesa da ${fromAction} a ${toAction} su ${cards}` : `ha modificato la regola di spesa da ${fromAction} a ${toAction}`, + `ha modificato la regola di spesa da ${fromAction} a ${toAction} su ${cards}`, appliedToAdditionalCards: ({count}: {count: number}) => `regola di spesa applicata a ${count} carte aggiuntive`, - phraseVerb: {added: 'aggiunto', removed: 'rimosso', changed: 'modificato', set: 'imposta', applied: 'applicato'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} esercente '${value}'` : `esercente '${value}'`), + phraseVerb: {added: 'aggiunto', removed: 'rimosso', changed: 'modificato', set: 'imposta', applied: 'applicata'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `Commerciante ${adjective} '${value}'` : `esercente '${value}'`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} esercente da '${oldValue}' a '${newValue}'` : `esercente da '${oldValue}' a '${newValue}'`, + adjective !== '' ? `commerciante ${adjective} da '${oldValue}' a '${newValue}'` : `esercente da '${oldValue}' a '${newValue}'`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => - adjective !== '' ? `categoria di spesa ${adjective} "${value}"` : `categoria di spesa '${value}'`, + adjective !== '' ? `Categoria di spesa ${adjective} "${value}"` : `categoria di spesa "${value}"`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `categoria di spesa ${adjective} da '${oldValue}' a '${newValue}'` : `categoria di spesa da '${oldValue}' a '${newValue}'`, bodyMaxAmount: 'importo massimo', - bodyMaxAmountSet: ({value}: {value: string}) => `importo massimo pari a ${value}`, + bodyMaxAmountSet: ({value}: {value: string}) => `importo massimo a ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `importo massimo da ${oldValue} a ${newValue}`, bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regola di spesa per ${count} carte aggiuntive`, bodyRemovedFromCards: ({cards}: {cards: string}) => `regola di spesa da ${cards}`, - composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} su ${cards}` : content), - composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} da ${cards}` : content), + composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} su ${cards}`, + composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} da ${cards}`, }, }, }, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 628bf8deb75e..9162e4b87076 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7667,10 +7667,13 @@ ${reportName} updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? '有効' : '無効'} の法人カード購入要件`, expensifyCardRule: { actionVerb: {block: 'ブロック済み', allow: '許可済み'}, - amountOperator: {over: '終了', under: '以下の条件のもと'}, + amountOperator: { + over: '上限', + under: '以下のいずれかで使われることが多いです。文脈に応じて変わります。\n\n- 〜の下に(位置・階層を表す場合)\n- 〜未満(数値・金額の場合)\n- 〜のもとで(条件・権限の場合)', + }, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `金額 ${operator} ${amount}`, theCard: 'カード', - multipleCards: ({count}: {count: number}) => `${count} 件のカード`, + multipleCards: ({count}: {count: number}) => `${count} 枚のカード`, joinFilters: ({items}: {items: string[]}) => { if (items.length === 0) { return ''; @@ -7681,38 +7684,35 @@ ${reportName} if (items.length === 2) { return `${items.at(0)} と ${items.at(1)}`; } - return `${items.slice(0, -1).join(', ')}、${items.at(-1)}`; + return `${items.slice(0, -1).join(', ')}、および ${items.at(-1)}`; }, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { - text += `${text === '' ? '' : ' '}${filters}`; - } - if (cards !== '') { - text += `${cards}での${text === '' ? '' : ' '}`; + text += ` ${filters}`; } + text += `${cards}で`; return text; }, - removeRule: ({cards}: {cards: string}) => (cards !== '' ? `${cards} から支出ルールを削除しました` : '支出ルールを削除しました'), + removeRule: ({cards}: {cards: string}) => `${cards} から支出ルールを削除しました`, restrictionVerb: {block: 'ブロック', allow: 'のみ許可'}, update: { - modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => - cards !== '' ? `${cards} の支出ルールを ${fromAction} から ${toAction} に変更しました` : `支出ルールを${fromAction}から${toAction}に変更しました`, + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `${cards} の支出ルールを ${fromAction} から ${toAction} に変更しました`, appliedToAdditionalCards: ({count}: {count: number}) => `${count} 枚の追加カードに支出ルールを適用しました`, - phraseVerb: {added: '追加済み', removed: '削除済み', changed: '変更済み', set: '設定', applied: '適用済み'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective}なマーチャント「${value}」` : `加盟店「${value}」`), + phraseVerb: {added: '追加しました', removed: '削除済み', changed: '変更しました', set: '設定', applied: '適用済み'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} な加盟店「${value}」` : `加盟店「${value}」`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective}加盟店を「${oldValue}」から「${newValue}」に変更しました` : `加盟店名を「${oldValue}」から「${newValue}」に変更`, - bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective}支出カテゴリー「${value}」` : `支出カテゴリ「${value}」`), + adjective !== '' ? `${adjective} のマーチャントを「${oldValue}」から「${newValue}」に変更しました` : `加盟店を「${oldValue}」から「${newValue}」に変更しました`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective}支出カテゴリ「${value}」` : `支出カテゴリ「${value}」`), bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective}支出カテゴリを「${oldValue}」から「${newValue}」に変更しました` : `支出カテゴリを「${oldValue}」から「${newValue}」に変更`, + adjective !== '' ? `${adjective}の支出カテゴリを「${oldValue}」から「${newValue}」に変更しました` : `支出カテゴリを「${oldValue}」から「${newValue}」に変更しました`, bodyMaxAmount: '最大金額', bodyMaxAmountSet: ({value}: {value: string}) => `最大金額を${value}に設定`, - bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `最大金額を${oldValue}から${newValue}に変更`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `${count} 枚の追加カードに支出ルールを適用`, - bodyRemovedFromCards: ({cards}: {cards: string}) => `${cards}の支出ルール`, - composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${cards}の${content}` : content), - composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${cards} からの ${content}` : content), + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `上限金額を${oldValue}から${newValue}に変更しました`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `追加のカード ${count} 枚に支出ルールを適用します`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `${cards} からの支出ルール`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => `${cards} の ${content}`, + composeFromCards: ({content, cards}: {content: string; cards: string}) => `${cards} からの ${content}`, }, }, }, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index ffeac3175dc7..52ffa2f7985b 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7747,39 +7747,37 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, if (items.length === 2) { return `${items.at(0)} en ${items.at(1)}`; } - return `${items.slice(0, -1).join(', ')} en ${items.at(-1)}`; + return `${items.slice(0, -1).join(', ')}, en ${items.at(-1)}`; }, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { - text += `${text === '' ? '' : ' '}${filters}`; - } - if (cards !== '') { - text += `${text === '' ? '' : ' '}op ${cards}`; + text += ` ${filters}`; } + text += `op ${cards}`; return text; }, - removeRule: ({cards}: {cards: string}) => (cards !== '' ? `uitgavenregel verwijderd van ${cards}` : 'uitgave-regel verwijderd'), + removeRule: ({cards}: {cards: string}) => `besteedregel verwijderd van ${cards}`, restrictionVerb: {block: 'blokkeren', allow: 'alleen toestaan'}, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => - cards !== '' ? `uitgave-regel gewijzigd van ${fromAction} naar ${toAction} op ${cards}` : `heeft bestedingsregel gewijzigd van ${fromAction} naar ${toAction}`, + `heeft de bestedingsregel gewijzigd van ${fromAction} naar ${toAction} op ${cards}`, appliedToAdditionalCards: ({count}: {count: number}) => `bestedingsregel toegepast op ${count} extra kaarten`, phraseVerb: {added: 'toegevoegd', removed: 'verwijderd', changed: 'gewijzigd', set: 'instellen', applied: 'toegepast'}, bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} handelaar '${value}'` : `handelaar '${value}'`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} handelaar van '${oldValue}' naar '${newValue}'` : `handelaar van '${oldValue}' naar '${newValue}'`, + adjective !== '' ? `${adjective} handelaar gewijzigd van '${oldValue}' naar '${newValue}'` : `handelaar van '${oldValue}' naar '${newValue}'`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => - adjective !== '' ? `${adjective} uitgavencategorie '${value}'` : `uitgavencategorie '${value}'`, + adjective !== '' ? `${adjective} uitgavencategorie '${value}'` : `uitgavencategorie ‘${value}’`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} uitgavencategorie van '${oldValue}' naar '${newValue}'` : `uitgavecategorie van '${oldValue}' naar '${newValue}'`, - bodyMaxAmount: 'max. bedrag', + adjective !== '' ? `${adjective} uitgavencategorie van '${oldValue}' naar '${newValue}'` : `bestedingscategorie van '${oldValue}' naar '${newValue}'`, + bodyMaxAmount: 'maximumbedrag', bodyMaxAmountSet: ({value}: {value: string}) => `max. bedrag tot ${value}`, - bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `max. bedrag van ${oldValue} naar ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `bestedsregel naar ${count} extra kaarten`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `maximumbedrag van ${oldValue} naar ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `bestedingsregel naar ${count} extra kaarten`, bodyRemovedFromCards: ({cards}: {cards: string}) => `bestedingsregel van ${cards}`, - composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} op ${cards}` : content), - composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} van ${cards}` : content), + composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} op ${cards}`, + composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} van ${cards}`, }, }, }, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 9a47b86c50e3..4423d6ac0e61 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7724,7 +7724,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, `dodano pole raportu typu ${fieldType} „${fieldName}”${defaultValue ? ` z domyślną wartością „${defaultValue}”` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'włączone' : 'wyłączone'} wymóg dotyczący zakupów kartą służbową`, expensifyCardRule: { - actionVerb: {block: 'zablokowano', allow: 'dozwolone'}, + actionVerb: {block: 'zablokowane', allow: 'dozwolone'}, amountOperator: {over: 'ponad', under: 'pod'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `kwoty ${operator} ${amount}`, theCard: 'karta', @@ -7744,34 +7744,32 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { - text += `${text === '' ? '' : ' '}${filters}`; - } - if (cards !== '') { - text += `${text === '' ? '' : ' '} na ${cards}`; + text += ` ${filters}`; } + text += `na ${cards}`; return text; }, - removeRule: ({cards}: {cards: string}) => (cards !== '' ? `usunięto regułę wydatków z ${cards}` : 'usunięto regułę wydatków'), + removeRule: ({cards}: {cards: string}) => `usunięto regułę wydatków z ${cards}`, restrictionVerb: {block: 'zablokuj', allow: 'zezwól tylko'}, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => - cards !== '' ? `zmieniono regułę wydatków z ${fromAction} na ${toAction} na ${cards}` : `zmienił(a) regułę wydatków z ${fromAction} na ${toAction}`, + `zmienił(-a) regułę wydatków z ${fromAction} na ${toAction} na ${cards}`, appliedToAdditionalCards: ({count}: {count: number}) => `zastosowano regułę wydatków do ${count} dodatkowych kart`, phraseVerb: {added: 'dodano', removed: 'usunięto', changed: 'zmieniono', set: 'ustaw', applied: 'zastosowano'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} sprzedawca „${value}”` : `sprzedawca „${value}”`), + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} sprzedawca '${value}'` : `sprzedawca „${value}”`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `${adjective} sprzedawcę z „${oldValue}” na „${newValue}”` : `sprzedawcę z „${oldValue}” na „${newValue}”`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => - adjective !== '' ? `kategoria wydatków ${adjective} „${value}”` : `kategoria wydatków „${value}”`, + adjective !== '' ? `${adjective} kategoria wydatków „${value}”` : `kategoria wydatków „${value}”`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `${adjective} kategorię wydatków z „${oldValue}” na „${newValue}”` : `kategoria wydatku z „${oldValue}” na „${newValue}”`, bodyMaxAmount: 'maksymalna kwota', bodyMaxAmountSet: ({value}: {value: string}) => `maksymalna kwota do ${value}`, - bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `maksymalna kwota z ${oldValue} na ${newValue}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `maksymalną kwotę z ${oldValue} na ${newValue}`, bodyAppliedToAdditionalCards: ({count}: {count: number}) => `zasada wydatków dla ${count} dodatkowych kart`, bodyRemovedFromCards: ({cards}: {cards: string}) => `reguła wydatków z ${cards}`, - composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} na ${cards}` : content), - composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} z ${cards}` : content), + composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} na ${cards}`, + composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} z ${cards}`, }, }, }, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 9bf3df8441c0..d3d50e6d14ce 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7725,7 +7725,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'ativado' : 'desativado'} o requisito de compras com cartão corporativo`, expensifyCardRule: { actionVerb: {block: 'bloqueado', allow: 'permitido'}, - amountOperator: {over: 'sobre', under: 'abaixo'}, + amountOperator: {over: 'sobre', under: 'embaixo'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `valores ${operator} ${amount}`, theCard: 'o cartão', multipleCards: ({count}: {count: number}) => `${count} cartões`, @@ -7739,39 +7739,37 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, if (items.length === 2) { return `${items.at(0)} e ${items.at(1)}`; } - return `${items.slice(0, -1).join(', ')}, e ${items.at(-1)}`; + return `${items.slice(0, -1).join(', ')} e ${items.at(-1)}`; }, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { - text += `${text === '' ? '' : ' '}${filters}`; - } - if (cards !== '') { - text += `${text === '' ? '' : ' '}em ${cards}`; + text += ` ${filters}`; } + text += `em ${cards}`; return text; }, - removeRule: ({cards}: {cards: string}) => (cards !== '' ? `removeu a regra de gasto de ${cards}` : 'removeu a regra de gasto'), + removeRule: ({cards}: {cards: string}) => `removeu a regra de gasto de ${cards}`, restrictionVerb: {block: 'bloquear', allow: 'permitir somente'}, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => - cards !== '' ? `alterou a regra de gasto de ${fromAction} para ${toAction} em ${cards}` : `alterou a regra de gasto de ${fromAction} para ${toAction}`, + `alterou a regra de gasto de ${fromAction} para ${toAction} em ${cards}`, appliedToAdditionalCards: ({count}: {count: number}) => `regra de gasto aplicada a mais ${count} cartões`, phraseVerb: {added: 'adicionado', removed: 'removido', changed: 'alterado', set: 'definir', applied: 'aplicado'}, bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `Comerciante ${adjective} '${value}'` : `estabelecimento '${value}'`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} comerciante de '${oldValue}' para '${newValue}'` : `estabelecimento comercial de '${oldValue}' para '${newValue}'`, + adjective !== '' ? `${adjective} comerciante de '${oldValue}' para '${newValue}'` : `estabelecimento de '${oldValue}' para '${newValue}'`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => - adjective !== '' ? `categoria de gasto ${adjective} '${value}'` : `categoria de despesa '${value}'`, + adjective !== '' ? `categoria de gasto ${adjective} '${value}'` : `categoria de gasto '${value}'`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `categoria de gasto ${adjective} de '${oldValue}' para '${newValue}'` : `categoria de gasto de '${oldValue}' para '${newValue}'`, bodyMaxAmount: 'valor máximo', - bodyMaxAmountSet: ({value}: {value: string}) => `valor máximo de ${value}`, + bodyMaxAmountSet: ({value}: {value: string}) => `valor máximo para ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `valor máximo de ${oldValue} para ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regra de gasto para ${count} cartões adicionais`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regra de gasto para mais ${count} cartões adicionais`, bodyRemovedFromCards: ({cards}: {cards: string}) => `regra de gasto de ${cards}`, - composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} em ${cards}` : content), - composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} de ${cards}` : content), + composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} em ${cards}`, + composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} de ${cards}`, }, }, }, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 2166b8d7c3a1..978f839d4c48 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7525,8 +7525,8 @@ ${reportName} addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `已添加 ${fieldType} 报告字段“${fieldName}”${defaultValue ? ` 默认值为“${defaultValue}”` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? '已启用' : '已禁用'} 公司商务卡消费要求`, expensifyCardRule: { - actionVerb: {block: '已阻止', allow: '允许'}, - amountOperator: {over: '结束', under: '在…之下'}, + actionVerb: {block: '已阻止', allow: '已允许'}, + amountOperator: {over: '超过', under: '在……之下'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `金额 ${operator} ${amount}`, theCard: '该卡', multipleCards: ({count}: {count: number}) => `${count} 张卡片`, @@ -7545,33 +7545,30 @@ ${reportName} addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { - text += `${text === '' ? '' : ' '}${filters}`; - } - if (cards !== '') { - text += `${cards} 上的 ${text === '' ? '' : ' '}`; + text += ` ${filters}`; } + text += `于 ${cards}`; return text; }, - removeRule: ({cards}: {cards: string}) => (cards !== '' ? `已从${cards}中移除消费规则` : '已移除消费规则'), - restrictionVerb: {block: '封锁', allow: '仅允许'}, + removeRule: ({cards}: {cards: string}) => `已从 ${cards} 中移除消费规则`, + restrictionVerb: {block: '屏蔽', allow: '仅允许'}, update: { - modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => - cards !== '' ? `已将 ${cards} 的消费规则从 ${fromAction} 更改为 ${toAction}` : `将消费规则从 ${fromAction} 更改为 ${toAction}`, + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `已将 ${cards} 的消费规则从 ${fromAction} 更改为 ${toAction}`, appliedToAdditionalCards: ({count}: {count: number}) => `已将消费规则应用到另外 ${count} 张卡片`, phraseVerb: {added: '已添加', removed: '已移除', changed: '已更改', set: '设置', applied: '已应用'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} 商户“${value}”` : `商户“${value}”`), + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} 商户“${value}”` : `商家“${value}”`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `将${adjective}商户从“${oldValue}”更改为“${newValue}”` : `商户从“${oldValue}”变更为“${newValue}”`, + adjective !== '' ? `将商家${adjective}从“${oldValue}”更改为“${newValue}”` : `商户从“${oldValue}”变为“${newValue}”`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} 支出类别“${value}”` : `支出类别“${value}”`), bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `将${adjective}支出类别从“${oldValue}”更改为“${newValue}”` : `将支出类别从“${oldValue}”更改为“${newValue}”`, - bodyMaxAmount: '最高金额', - bodyMaxAmountSet: ({value}: {value: string}) => `最大金额为 ${value}`, - bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `最大金额从 ${oldValue} 变更为 ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `将消费规则应用到另外 ${count} 张卡片`, - bodyRemovedFromCards: ({cards}: {cards: string}) => `来自${cards}的支出规则`, - composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${cards} 上的 ${content}` : content), - composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `来自${cards}的${content}` : content), + bodyMaxAmount: '最大金额', + bodyMaxAmountSet: ({value}: {value: string}) => `最高金额为 ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `最大金额从 ${oldValue} 变为 ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `为另外 ${count} 张卡设置消费规则`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `来自 ${cards} 的消费规则`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => `${cards} 上的 ${content}`, + composeFromCards: ({content, cards}: {content: string; cards: string}) => `来自${cards}的${content}`, }, }, }, From a520de1216f421a543cfc1b07923692517a47b9c Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 22 May 2026 10:21:41 -0300 Subject: [PATCH 25/43] dry code --- src/libs/ReportActionsUtils.ts | 115 +++++++++++++++++---------------- 1 file changed, 58 insertions(+), 57 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 06788296931c..3bcab9b51140 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -4149,6 +4149,46 @@ function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAct return translate('workspaceActions.expensifyCardRule.addRule', {verb, filters, cards: cardsSummary}); } +function pushDiffPhrases( + phrases: SpendRulePhrase[], + diff: SpendRuleStringDiff, + adjective: SpendRulePhraseAdjective, + adjectiveWord: string, + getDisplayName: (value: string) => string, + formatBody: (params: {adjective: string; value: string}) => string, + formatBodyChange: (params: {adjective: string; oldValue: string; newValue: string}) => string, +): void { + if (diff.added.length === 1 && diff.removed.length === 1) { + const oldValue = getDisplayName(diff.removed.at(0) ?? ''); + const newValue = getDisplayName(diff.added.at(0) ?? ''); + phrases.push({ + verb: 'changed', + adjective, + bodyWithAdjective: formatBodyChange({adjective: adjectiveWord, oldValue, newValue}), + bodyWithoutAdjective: formatBodyChange({adjective: '', oldValue, newValue}), + }); + } else { + for (const value of diff.added) { + const display = getDisplayName(value); + phrases.push({ + verb: 'added', + adjective, + bodyWithAdjective: formatBody({adjective: adjectiveWord, value: display}), + bodyWithoutAdjective: formatBody({adjective: '', value: display}), + }); + } + for (const value of diff.removed) { + const display = getDisplayName(value); + phrases.push({ + verb: 'removed', + adjective, + bodyWithAdjective: formatBody({adjective: adjectiveWord, value: display}), + bodyWithoutAdjective: formatBody({adjective: '', value: display}), + }); + } + } +} + function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE)) { return ''; @@ -4202,63 +4242,24 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report const adjectiveWord = getSpendRuleActionVerb(translate, adjective); const phrases: SpendRulePhrase[] = []; - if (merchantDiff.added.length === 1 && merchantDiff.removed.length === 1) { - const oldValue = merchantDiff.removed.at(0) ?? ''; - const newValue = merchantDiff.added.at(0) ?? ''; - phrases.push({ - verb: 'changed', - adjective, - bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchantChange', {adjective: adjectiveWord, oldValue, newValue}), - bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchantChange', {adjective: '', oldValue, newValue}), - }); - } else { - for (const merchant of merchantDiff.added) { - phrases.push({ - verb: 'added', - adjective, - bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchant', {adjective: adjectiveWord, value: merchant}), - bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchant', {adjective: '', value: merchant}), - }); - } - for (const merchant of merchantDiff.removed) { - phrases.push({ - verb: 'removed', - adjective, - bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchant', {adjective: adjectiveWord, value: merchant}), - bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchant', {adjective: '', value: merchant}), - }); - } - } - - if (categoryDiff.added.length === 1 && categoryDiff.removed.length === 1) { - const oldValue = getSpendRuleCategoryDisplayName(translate, categoryDiff.removed.at(0) ?? ''); - const newValue = getSpendRuleCategoryDisplayName(translate, categoryDiff.added.at(0) ?? ''); - phrases.push({ - verb: 'changed', - adjective, - bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategoryChange', {adjective: adjectiveWord, oldValue, newValue}), - bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategoryChange', {adjective: '', oldValue, newValue}), - }); - } else { - for (const category of categoryDiff.added) { - const value = getSpendRuleCategoryDisplayName(translate, category); - phrases.push({ - verb: 'added', - adjective, - bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', {adjective: adjectiveWord, value}), - bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', {adjective: '', value}), - }); - } - for (const category of categoryDiff.removed) { - const value = getSpendRuleCategoryDisplayName(translate, category); - phrases.push({ - verb: 'removed', - adjective, - bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', {adjective: adjectiveWord, value}), - bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', {adjective: '', value}), - }); - } - } + pushDiffPhrases( + phrases, + merchantDiff, + adjective, + adjectiveWord, + (value) => value, + (params) => translate('workspaceActions.expensifyCardRule.update.bodyMerchant', params), + (params) => translate('workspaceActions.expensifyCardRule.update.bodyMerchantChange', params), + ); + pushDiffPhrases( + phrases, + categoryDiff, + adjective, + adjectiveWord, + (category) => getSpendRuleCategoryDisplayName(translate, category), + (params) => translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', params), + (params) => translate('workspaceActions.expensifyCardRule.update.bodySpendCategoryChange', params), + ); if (amountDiff.added.length === 1 && amountDiff.removed.length === 1) { const oldValue = formatSpendRuleAmount(amountDiff.removed.at(0) ?? {value: []}, currency); From 27db4afea9632824e46f739cb3b03c8563f261b2 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 26 May 2026 12:29:41 -0600 Subject: [PATCH 26/43] rename functions --- src/libs/ReportActionsUtils.ts | 18 +- tests/unit/ReportActionsUtilsTest.ts | 305 +++++++++++++++++++++++++++ 2 files changed, 314 insertions(+), 9 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 36c60abc7590..bca4c5871af3 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3928,7 +3928,7 @@ function getSpendRuleActionVerb(translate: LocalizedTranslate, action: string): return ''; } -function spendRuleAmountOperatorWord(translate: LocalizedTranslate, operator: string): string { +function getSpendRuleAmountOperatorWord(translate: LocalizedTranslate, operator: string): string { if (operator === CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO) { return translate('workspaceActions.expensifyCardRule.amountOperator.under'); } @@ -3939,7 +3939,7 @@ function spendRuleAmountOperatorWord(translate: LocalizedTranslate, operator: st } function getSpendRuleAmountString(translate: LocalizedTranslate, amount: {operator: string; value: string[]}, currency: string): string { - const operatorWord = spendRuleAmountOperatorWord(translate, amount.operator); + const operatorWord = getSpendRuleAmountOperatorWord(translate, amount.operator); if (amount.value.length === 0) { return ''; } @@ -4027,7 +4027,7 @@ function computeSpendRuleAmountDiff(oldAmounts: SpendRuleAmount[], newAmounts: S type SpendRuleCard = {cardID?: number | string; displayName?: string}; type SpendRuleCardDiff = {added: SpendRuleCard[]; removed: SpendRuleCard[]}; -function spendRuleCardID(card: SpendRuleCard): number | undefined { +function getSpendRuleCardID(card: SpendRuleCard): number | undefined { const cardID = card?.cardID; if (typeof cardID === 'number' && Number.isFinite(cardID)) { return cardID; @@ -4041,14 +4041,14 @@ function spendRuleCardID(card: SpendRuleCard): number | undefined { function computeSpendRuleCardDiff(oldCards: SpendRuleCard[], newCards: SpendRuleCard[]): SpendRuleCardDiff { const oldByID = new Map(); for (const card of oldCards) { - const id = spendRuleCardID(card); + const id = getSpendRuleCardID(card); if (id !== undefined) { oldByID.set(id, card); } } const newByID = new Map(); for (const card of newCards) { - const id = spendRuleCardID(card); + const id = getSpendRuleCardID(card); if (id !== undefined) { newByID.set(id, card); } @@ -4078,7 +4078,7 @@ type SpendRulePhrase = { bodyWithoutAdjective: string; }; -function spendRulePhraseVerbWord(translate: LocalizedTranslate, verb: SpendRulePhraseVerb): string { +function getSpendRulePhraseVerbWord(translate: LocalizedTranslate, verb: SpendRulePhraseVerb): string { return translate(`workspaceActions.expensifyCardRule.update.phraseVerb.${verb}`); } @@ -4091,14 +4091,14 @@ function joinSpendRulePhrases(translate: LocalizedTranslate, phrases: readonly S if (!phrase) { return ''; } - return `${spendRulePhraseVerbWord(translate, phrase.verb)} ${phrase.bodyWithAdjective}`; + return `${getSpendRulePhraseVerbWord(translate, phrase.verb)} ${phrase.bodyWithAdjective}`; } const firstVerb = phrases.at(0)?.verb; const allSameVerb = firstVerb !== undefined && phrases.every((phrase) => phrase.verb === firstVerb); if (!allSameVerb) { - const parts = phrases.map((phrase) => `${spendRulePhraseVerbWord(translate, phrase.verb)} ${phrase.bodyWithAdjective}`); + const parts = phrases.map((phrase) => `${getSpendRulePhraseVerbWord(translate, phrase.verb)} ${phrase.bodyWithAdjective}`); return getSpendRuleJoinFilters(translate, parts); } @@ -4107,7 +4107,7 @@ function joinSpendRulePhrases(translate: LocalizedTranslate, phrases: readonly S return ''; } const firstAdjective = firstPhrase.adjective; - const parts: string[] = [`${spendRulePhraseVerbWord(translate, firstPhrase.verb)} ${firstPhrase.bodyWithAdjective}`]; + const parts: string[] = [`${getSpendRulePhraseVerbWord(translate, firstPhrase.verb)} ${firstPhrase.bodyWithAdjective}`]; for (let i = 1; i < phrases.length; i++) { const phrase = phrases.at(i); if (!phrase) { diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 9f6394f4e198..18ca5ad35a0f 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -14,6 +14,7 @@ import CONST from '../../src/CONST'; import * as ReportActionsUtils from '../../src/libs/ReportActionsUtils'; import { getAddedCardFeedMessage, + getAddExpensifyCardRuleMessage, getAssignedCompanyCardMessage, getAutoPayApprovedReportsEnabledMessage, getAutoReimbursementMessage, @@ -32,6 +33,8 @@ import { getModerationFlagState, getOneTransactionThreadReportID, getOriginalMessage, + getRemoveExpensifyCardRuleMessage, + getUpdateExpensifyCardRuleMessage, getPolicyChangeLogMaxExpenseAgeMessage, getPolicyChangeLogMaxExpenseAmountMessage, getPolicyChangeLogMaxExpenseAmountNoItemizedReceiptMessage, @@ -5394,4 +5397,306 @@ describe('ReportActionsUtils', () => { expect(getModerationFlagState(action)).toEqual({latestDecision: undefined, hasBeenFlagged: false}); }); }); + + describe('getAddExpensifyCardRuleMessage', () => { + it('returns empty string for wrong action type', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CARD_FEED, + reportActionID: '1', + created: '', + originalMessage: {}, + } as ReportAction; + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe(''); + }); + + it('returns allow message with no filters and no named card', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.ALLOW, + cards: [], + }, + } as ReportAction; + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe('allowed on the card'); + }); + + it('returns allow message with single named card and no filters', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.ALLOW, + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe('allowed on My Visa'); + }); + + it('returns block message with merchant filter', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.BLOCK, + merchants: ['Starbucks'], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("blocked 'Starbucks' on My Visa"); + }); + + it('returns allow message with category filter', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.ALLOW, + categories: [CONST.SPEND_RULES.CATEGORIES.DINING], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("allowed 'Dining' on My Visa"); + }); + + it('returns block message with amount-over filter', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.BLOCK, + currency: CONST.CURRENCY.USD, + amounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN, value: ['100.00']}], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe('blocked amounts over $100.00 on My Visa'); + }); + + it('returns allow message with merchant and amount-under filter joined', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.ALLOW, + currency: CONST.CURRENCY.USD, + merchants: ['Amazon'], + amounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO, value: ['50.00']}], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("allowed 'Amazon' and amounts under $50.00 on My Visa"); + }); + + it('returns message with multiple-cards summary', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.ALLOW, + cards: [ + {cardID: 1, displayName: 'Card A'}, + {cardID: 2, displayName: 'Card B'}, + ], + }, + } as ReportAction; + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe('allowed on 2 cards'); + }); + }); + + describe('getUpdateExpensifyCardRuleMessage', () => { + it('returns empty string for wrong action type', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: {}, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe(''); + }); + + it('returns mode-change message when only action changed', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + oldAction: CONST.SPEND_RULES.ACTION.ALLOW, + action: CONST.SPEND_RULES.ACTION.BLOCK, + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('changed spend rule from only allow to block on My Visa'); + }); + + it('returns applied-to-additional-cards message when only new cards were added', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.ALLOW, + oldCards: [{cardID: 1, displayName: 'Card A'}], + cards: [ + {cardID: 1, displayName: 'Card A'}, + {cardID: 2, displayName: 'Card B'}, + ], + }, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('applied spend rule to 1 additional cards'); + }); + + it('returns remove-rule message when only cards were removed', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.ALLOW, + oldCards: [ + {cardID: 1, displayName: 'Card A'}, + {cardID: 2, displayName: 'Card B'}, + ], + cards: [{cardID: 1, displayName: 'Card A'}], + }, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('removed spend rule from Card B'); + }); + + it('returns added-merchant message with adjective for allow action', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.ALLOW, + oldMerchants: [], + merchants: ['Starbucks'], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe("added allowed merchant 'Starbucks' on My Visa"); + }); + + it('returns changed-category message when single category was swapped', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.BLOCK, + oldCategories: [CONST.SPEND_RULES.CATEGORIES.AIRLINES], + categories: [CONST.SPEND_RULES.CATEGORIES.DINING], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe("changed blocked spend category from 'Airlines' to 'Dining' on My Visa"); + }); + + it('returns set-max-amount message when amount is newly added', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.BLOCK, + currency: CONST.CURRENCY.USD, + oldAmounts: [], + amounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN, value: ['100.00']}], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('set max amount to $100.00 on My Visa'); + }); + + it('returns changed-max-amount message when amount value changes', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.BLOCK, + currency: CONST.CURRENCY.USD, + oldAmounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN, value: ['50.00']}], + amounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN, value: ['100.00']}], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('changed max amount from $50.00 to $100.00 on My Visa'); + }); + + it('returns removed-max-amount message when amount is cleared', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.BLOCK, + currency: CONST.CURRENCY.USD, + oldAmounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN, value: ['100.00']}], + amounts: [], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('removed max amount from My Visa'); + }); + }); + + describe('getRemoveExpensifyCardRuleMessage', () => { + it('returns empty string for wrong action type', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: {}, + } as ReportAction; + expect(getRemoveExpensifyCardRuleMessage(translateLocal, action)).toBe(''); + }); + + it('returns message with single named card', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getRemoveExpensifyCardRuleMessage(translateLocal, action)).toBe('removed spend rule from My Visa'); + }); + + it('returns message with multiple-cards summary', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + cards: [ + {cardID: 1, displayName: 'Card A'}, + {cardID: 2, displayName: 'Card B'}, + ], + }, + } as ReportAction; + expect(getRemoveExpensifyCardRuleMessage(translateLocal, action)).toBe('removed spend rule from 2 cards'); + }); + + it('returns message with fallback "the card" when cards list is empty', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + cards: [], + }, + } as ReportAction; + expect(getRemoveExpensifyCardRuleMessage(translateLocal, action)).toBe('removed spend rule from the card'); + }); + }); }); From 8b4e2bd107be7ecf65f5be9175488b3a6e649c95 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 26 May 2026 12:33:41 -0600 Subject: [PATCH 27/43] create util file, add tests --- src/libs/ReportActionsUtils.ts | 409 +------------------- src/libs/SpendRuleChangeLogUtils.ts | 436 ++++++++++++++++++++++ tests/unit/ReportActionsUtilsTest.ts | 305 --------------- tests/unit/SpendRuleChangeLogUtilsTest.ts | 321 ++++++++++++++++ 4 files changed, 759 insertions(+), 712 deletions(-) create mode 100644 src/libs/SpendRuleChangeLogUtils.ts create mode 100644 tests/unit/SpendRuleChangeLogUtilsTest.ts diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index bca4c5871af3..34b7b61e21da 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3918,410 +3918,6 @@ function getUpdatedApprovalRuleMessage(translate: LocalizedTranslate, reportActi return getReportActionText(reportAction); } -function getSpendRuleActionVerb(translate: LocalizedTranslate, action: string): string { - if (action === CONST.SPEND_RULES.ACTION.BLOCK) { - return translate('workspaceActions.expensifyCardRule.actionVerb.block'); - } - if (action === CONST.SPEND_RULES.ACTION.ALLOW) { - return translate('workspaceActions.expensifyCardRule.actionVerb.allow'); - } - return ''; -} - -function getSpendRuleAmountOperatorWord(translate: LocalizedTranslate, operator: string): string { - if (operator === CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO) { - return translate('workspaceActions.expensifyCardRule.amountOperator.under'); - } - if (operator === CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN) { - return translate('workspaceActions.expensifyCardRule.amountOperator.over'); - } - return ''; -} - -function getSpendRuleAmountString(translate: LocalizedTranslate, amount: {operator: string; value: string[]}, currency: string): string { - const operatorWord = getSpendRuleAmountOperatorWord(translate, amount.operator); - if (amount.value.length === 0) { - return ''; - } - return translate('workspaceActions.expensifyCardRule.amountFilter', {operator: operatorWord, amount: formatSpendRuleAmount(amount, currency)}); -} - -function getSpendRuleCardsSummary(translate: LocalizedTranslate, cards: ReadonlyArray<{displayName?: string}> | undefined): string { - if (!cards || cards.length === 0) { - return translate('workspaceActions.expensifyCardRule.theCard'); - } - if (cards.length === 1) { - const displayName = cards.at(0)?.displayName ?? ''; - return displayName !== '' ? displayName : translate('workspaceActions.expensifyCardRule.theCard'); - } - return translate('workspaceActions.expensifyCardRule.multipleCards', {count: cards.length}); -} - -function getSpendRuleJoinFilters(translate: LocalizedTranslate, items: readonly string[]): string { - const filtered = items.filter((value) => typeof value === 'string' && value !== ''); - return translate('workspaceActions.expensifyCardRule.joinFilters', {items: filtered}); -} - -function getSpendRuleCategoryDisplayName(translate: LocalizedTranslate, category: string): string { - const knownCategories = Object.values(CONST.SPEND_RULES.CATEGORIES) as string[]; - if (knownCategories.includes(category)) { - return translate(`workspace.rules.spendRules.categoryOptions.${category as ValueOf}`); - } - return category; -} - -function getSpendRuleRestrictionVerb(translate: LocalizedTranslate, action: string): string { - if (action === CONST.SPEND_RULES.ACTION.BLOCK) { - return translate('workspaceActions.expensifyCardRule.restrictionVerb.block'); - } - if (action === CONST.SPEND_RULES.ACTION.ALLOW) { - return translate('workspaceActions.expensifyCardRule.restrictionVerb.allow'); - } - return action; -} - -function formatSpendRuleAmount(amount: {value: string[]}, currency: string): string { - return convertAmountToDisplayString(getSpendRuleValueInCents(amount.value), currency); -} - -type SpendRuleStringDiff = {added: string[]; removed: string[]}; - -function computeSpendRuleStringDiff(oldValues: string[], newValues: string[]): SpendRuleStringDiff { - const oldSet = Array.from(new Set(oldValues)); - const newSet = Array.from(new Set(newValues)); - const added = newSet.filter((value) => !oldSet.includes(value)).sort(); - const removed = oldSet.filter((value) => !newSet.includes(value)).sort(); - return {added, removed}; -} - -type SpendRuleAmount = {operator: string; value: string[]}; -type SpendRuleAmountDiff = {added: SpendRuleAmount[]; removed: SpendRuleAmount[]}; - -function getSpendRuleValueInCents(value: string[]): number { - const firstValue = value.at(0) ?? ''; - return firstValue !== '' && Number.isFinite(Number(firstValue)) ? Math.round(parseFloat(firstValue) * 100) : 0; -} - -function computeSpendRuleAmountDiff(oldAmounts: SpendRuleAmount[], newAmounts: SpendRuleAmount[]): SpendRuleAmountDiff { - const oldAmount = oldAmounts.at(0); - const newAmount = newAmounts.at(0); - if (!oldAmount && !newAmount) { - return {added: [], removed: []}; - } - if (!oldAmount) { - return {added: newAmount ? [newAmount] : [], removed: []}; - } - if (!newAmount) { - return {added: [], removed: [oldAmount]}; - } - const sameAmount = getSpendRuleValueInCents(oldAmount.value) === getSpendRuleValueInCents(newAmount.value); - if (sameAmount) { - return {added: [], removed: []}; - } - return { - added: [newAmount], - removed: [oldAmount], - }; -} - -type SpendRuleCard = {cardID?: number | string; displayName?: string}; -type SpendRuleCardDiff = {added: SpendRuleCard[]; removed: SpendRuleCard[]}; - -function getSpendRuleCardID(card: SpendRuleCard): number | undefined { - const cardID = card?.cardID; - if (typeof cardID === 'number' && Number.isFinite(cardID)) { - return cardID; - } - if (typeof cardID === 'string' && /^\d+$/.test(cardID)) { - return Number.parseInt(cardID, 10); - } - return undefined; -} - -function computeSpendRuleCardDiff(oldCards: SpendRuleCard[], newCards: SpendRuleCard[]): SpendRuleCardDiff { - const oldByID = new Map(); - for (const card of oldCards) { - const id = getSpendRuleCardID(card); - if (id !== undefined) { - oldByID.set(id, card); - } - } - const newByID = new Map(); - for (const card of newCards) { - const id = getSpendRuleCardID(card); - if (id !== undefined) { - newByID.set(id, card); - } - } - const added: SpendRuleCard[] = []; - for (const [id, card] of newByID) { - if (!oldByID.has(id)) { - added.push(card); - } - } - const removed: SpendRuleCard[] = []; - for (const [id, card] of oldByID) { - if (!newByID.has(id)) { - removed.push(card); - } - } - return {added, removed}; -} - -type SpendRulePhraseVerb = 'added' | 'removed' | 'changed' | 'set' | 'applied'; -type SpendRulePhraseAdjective = '' | typeof CONST.SPEND_RULES.ACTION.BLOCK | typeof CONST.SPEND_RULES.ACTION.ALLOW; - -type SpendRulePhrase = { - verb: SpendRulePhraseVerb; - adjective: SpendRulePhraseAdjective; - bodyWithAdjective: string; - bodyWithoutAdjective: string; -}; - -function getSpendRulePhraseVerbWord(translate: LocalizedTranslate, verb: SpendRulePhraseVerb): string { - return translate(`workspaceActions.expensifyCardRule.update.phraseVerb.${verb}`); -} - -function joinSpendRulePhrases(translate: LocalizedTranslate, phrases: readonly SpendRulePhrase[]): string { - if (phrases.length === 0) { - return ''; - } - if (phrases.length === 1) { - const phrase = phrases.at(0); - if (!phrase) { - return ''; - } - return `${getSpendRulePhraseVerbWord(translate, phrase.verb)} ${phrase.bodyWithAdjective}`; - } - - const firstVerb = phrases.at(0)?.verb; - const allSameVerb = firstVerb !== undefined && phrases.every((phrase) => phrase.verb === firstVerb); - - if (!allSameVerb) { - const parts = phrases.map((phrase) => `${getSpendRulePhraseVerbWord(translate, phrase.verb)} ${phrase.bodyWithAdjective}`); - return getSpendRuleJoinFilters(translate, parts); - } - - const firstPhrase = phrases.at(0); - if (!firstPhrase) { - return ''; - } - const firstAdjective = firstPhrase.adjective; - const parts: string[] = [`${getSpendRulePhraseVerbWord(translate, firstPhrase.verb)} ${firstPhrase.bodyWithAdjective}`]; - for (let i = 1; i < phrases.length; i++) { - const phrase = phrases.at(i); - if (!phrase) { - continue; - } - const useOwnAdjective = phrase.adjective !== '' && phrase.adjective !== firstAdjective; - parts.push(useOwnAdjective ? phrase.bodyWithAdjective : phrase.bodyWithoutAdjective); - } - return getSpendRuleJoinFilters(translate, parts); -} - -function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { - if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE)) { - return ''; - } - const message = getOriginalMessage(reportAction) ?? {}; - const action = message.action ?? CONST.SPEND_RULES.ACTION.ALLOW; - const currency = message.currency ?? CONST.CURRENCY.USD; - const merchants = message.merchants ?? []; - const categories = message.categories ?? []; - const amounts = message.amounts ?? []; - const cards = message.cards ?? []; - - const items: string[] = []; - for (const merchant of merchants) { - items.push(`'${merchant}'`); - } - for (const category of categories) { - items.push(`'${getSpendRuleCategoryDisplayName(translate, category)}'`); - } - for (const amount of amounts) { - const formattedAmount = getSpendRuleAmountString(translate, amount, currency); - if (formattedAmount !== '') { - items.push(formattedAmount); - } - } - - const verb = getSpendRuleActionVerb(translate, action); - const filters = getSpendRuleJoinFilters(translate, items); - const cardsSummary = getSpendRuleCardsSummary(translate, cards); - - if (verb === '') { - return getReportActionText(reportAction); - } - - return translate('workspaceActions.expensifyCardRule.addRule', {verb, filters, cards: cardsSummary}); -} - -function pushDiffPhrases( - phrases: SpendRulePhrase[], - diff: SpendRuleStringDiff, - adjective: SpendRulePhraseAdjective, - adjectiveWord: string, - getDisplayName: (value: string) => string, - formatBody: (params: {adjective: string; value: string}) => string, - formatBodyChange: (params: {adjective: string; oldValue: string; newValue: string}) => string, -): void { - if (diff.added.length === 1 && diff.removed.length === 1) { - const oldValue = getDisplayName(diff.removed.at(0) ?? ''); - const newValue = getDisplayName(diff.added.at(0) ?? ''); - phrases.push({ - verb: 'changed', - adjective, - bodyWithAdjective: formatBodyChange({adjective: adjectiveWord, oldValue, newValue}), - bodyWithoutAdjective: formatBodyChange({adjective: '', oldValue, newValue}), - }); - } else { - for (const value of diff.added) { - const display = getDisplayName(value); - phrases.push({ - verb: 'added', - adjective, - bodyWithAdjective: formatBody({adjective: adjectiveWord, value: display}), - bodyWithoutAdjective: formatBody({adjective: '', value: display}), - }); - } - for (const value of diff.removed) { - const display = getDisplayName(value); - phrases.push({ - verb: 'removed', - adjective, - bodyWithAdjective: formatBody({adjective: adjectiveWord, value: display}), - bodyWithoutAdjective: formatBody({adjective: '', value: display}), - }); - } - } -} - -function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { - if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE)) { - return ''; - } - const message = getOriginalMessage(reportAction) ?? {}; - const oldAction = message.oldAction ?? CONST.SPEND_RULES.ACTION.ALLOW; - const newAction = message.action ?? CONST.SPEND_RULES.ACTION.ALLOW; - const actionChanged = oldAction !== '' && oldAction !== newAction; - const currency = message.currency ?? CONST.CURRENCY.USD; - - const oldMerchants = message.oldMerchants ?? []; - const newMerchants = message.merchants ?? []; - const oldCategories = message.oldCategories ?? []; - const newCategories = message.categories ?? []; - const oldAmounts = message.oldAmounts ?? []; - const newAmounts = message.amounts ?? []; - const oldCards = message.oldCards ?? []; - const newCards = message.cards ?? []; - - const merchantDiff = computeSpendRuleStringDiff(oldMerchants, newMerchants); - const categoryDiff = computeSpendRuleStringDiff(oldCategories, newCategories); - const amountDiff = computeSpendRuleAmountDiff(oldAmounts, newAmounts); - const cardDiff = computeSpendRuleCardDiff(oldCards, newCards); - - const merchantsChanged = merchantDiff.added.length > 0 || merchantDiff.removed.length > 0; - const categoriesChanged = categoryDiff.added.length > 0 || categoryDiff.removed.length > 0; - const amountsChanged = amountDiff.added.length > 0 || amountDiff.removed.length > 0; - const cardsChanged = cardDiff.added.length > 0 || cardDiff.removed.length > 0; - const filtersAndCardsUnchanged = !merchantsChanged && !categoriesChanged && !amountsChanged && !cardsChanged; - - const newCardsSummary = getSpendRuleCardsSummary(translate, newCards); - - if (actionChanged && filtersAndCardsUnchanged) { - return translate('workspaceActions.expensifyCardRule.update.modeChange', { - fromAction: getSpendRuleRestrictionVerb(translate, oldAction), - toAction: getSpendRuleRestrictionVerb(translate, newAction), - cards: newCardsSummary, - }); - } - - if (cardsChanged && !merchantsChanged && !categoriesChanged && !amountsChanged && !actionChanged) { - if (cardDiff.added.length > 0 && cardDiff.removed.length === 0) { - return translate('workspaceActions.expensifyCardRule.update.appliedToAdditionalCards', {count: cardDiff.added.length}); - } - if (cardDiff.added.length === 0 && cardDiff.removed.length > 0) { - return translate('workspaceActions.expensifyCardRule.removeRule', {cards: getSpendRuleCardsSummary(translate, cardDiff.removed)}); - } - } - - const adjective: SpendRulePhraseAdjective = newAction === CONST.SPEND_RULES.ACTION.BLOCK || newAction === CONST.SPEND_RULES.ACTION.ALLOW ? newAction : ''; - const adjectiveWord = getSpendRuleActionVerb(translate, adjective); - const phrases: SpendRulePhrase[] = []; - - pushDiffPhrases( - phrases, - merchantDiff, - adjective, - adjectiveWord, - (value) => value, - (params) => translate('workspaceActions.expensifyCardRule.update.bodyMerchant', params), - (params) => translate('workspaceActions.expensifyCardRule.update.bodyMerchantChange', params), - ); - pushDiffPhrases( - phrases, - categoryDiff, - adjective, - adjectiveWord, - (category) => getSpendRuleCategoryDisplayName(translate, category), - (params) => translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', params), - (params) => translate('workspaceActions.expensifyCardRule.update.bodySpendCategoryChange', params), - ); - - if (amountDiff.added.length === 1 && amountDiff.removed.length === 1) { - const oldValue = formatSpendRuleAmount(amountDiff.removed.at(0) ?? {value: []}, currency); - const newValue = formatSpendRuleAmount(amountDiff.added.at(0) ?? {value: []}, currency); - const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountChange', {oldValue, newValue}); - phrases.push({verb: 'changed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); - } else { - for (const amount of amountDiff.added) { - const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountSet', {value: formatSpendRuleAmount(amount, currency)}); - phrases.push({verb: 'set', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); - } - if (amountDiff.removed.length > 0) { - const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmount'); - const removedPhrase: SpendRulePhrase = {verb: 'removed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}; - phrases.push(...Array.from({length: amountDiff.removed.length}).fill(removedPhrase)); - } - } - - if (cardDiff.added.length > 0) { - const body = translate('workspaceActions.expensifyCardRule.update.bodyAppliedToAdditionalCards', {count: cardDiff.added.length}); - phrases.push({verb: 'applied', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); - } - if (cardDiff.removed.length > 0) { - const body = translate('workspaceActions.expensifyCardRule.update.bodyRemovedFromCards', {cards: getSpendRuleCardsSummary(translate, cardDiff.removed)}); - phrases.push({verb: 'removed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); - } - - if (phrases.length === 0) { - return getReportActionText(reportAction); - } - - const joined = joinSpendRulePhrases(translate, phrases); - - if (cardsChanged) { - return joined; - } - - const onlyRemovedPhrase = phrases.length === 1 && phrases.at(0)?.verb === 'removed'; - if (onlyRemovedPhrase) { - return translate('workspaceActions.expensifyCardRule.update.composeFromCards', {content: joined, cards: newCardsSummary}); - } - - return translate('workspaceActions.expensifyCardRule.update.composeOnCards', {content: joined, cards: newCardsSummary}); -} - -function getRemoveExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { - if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE)) { - return ''; - } - const message = getOriginalMessage(reportAction) ?? {}; - const cards = message.cards ?? []; - const cardsSummary = getSpendRuleCardsSummary(translate, cards); - return translate('workspaceActions.expensifyCardRule.removeRule', {cards: cardsSummary}); -} - function getRemovedFromApprovalChainMessage(translate: LocalizedTranslate, reportAction: OnyxEntry>) { const originalMessage = getOriginalMessage(reportAction); const submittersNames = getPersonalDetailsByIDs({ @@ -4973,6 +4569,8 @@ function hasReasoning(action: OnyxInputOrEntry): boolean { return !!originalMessage && typeof originalMessage === 'object' && 'reasoning' in originalMessage && !!originalMessage.reasoning; } +export {getAddExpensifyCardRuleMessage, getRemoveExpensifyCardRuleMessage, getUpdateExpensifyCardRuleMessage} from './SpendRuleChangeLogUtils'; + export { doesReportHaveVisibleActions, extractLinksFromMessageHtml, @@ -5002,8 +4600,6 @@ export { getOneTransactionThreadReportAction, getOneTransactionThreadReportID, getOriginalMessage, - getAddExpensifyCardRuleMessage, - getUpdateExpensifyCardRuleMessage, getAddedApprovalRuleMessage, getDeletedApprovalRuleMessage, getUpdatedApprovalRuleMessage, @@ -5015,7 +4611,6 @@ export { getReportActionMessage, getReportActionMessageText, getReportActionText, - getRemoveExpensifyCardRuleMessage, getSortedReportActions, getSortedReportActionsForDisplay, isCardBrokenConnectionAction, diff --git a/src/libs/SpendRuleChangeLogUtils.ts b/src/libs/SpendRuleChangeLogUtils.ts new file mode 100644 index 000000000000..6cb1383e23c6 --- /dev/null +++ b/src/libs/SpendRuleChangeLogUtils.ts @@ -0,0 +1,436 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import type {LocalizedTranslate} from '@components/LocaleContextProvider'; +import CONST from '@src/CONST'; +import type ReportAction from '@src/types/onyx/ReportAction'; +import type {OriginalMessage} from '@src/types/onyx/ReportAction'; +import type ReportActionName from '@src/types/onyx/ReportActionName'; +import {convertAmountToDisplayString} from './CurrencyUtils'; +import Parser from './Parser'; +import stripFollowupListFromHtml from './ReportActionFollowupUtils/stripFollowupListFromHtml'; + +function isActionOfType(action: OnyxEntry, actionName: T): action is ReportAction { + return action?.actionName === actionName; +} + +function getOriginalMessage(reportAction: OnyxEntry>): OriginalMessage | undefined { + const candidate = !Array.isArray(reportAction?.message) ? (reportAction?.message ?? reportAction?.originalMessage) : reportAction?.originalMessage; + + if (candidate === null || typeof candidate !== 'object') { + return undefined; + } + return candidate as OriginalMessage; +} + +function getSpendRuleFallbackReportActionText(reportAction: OnyxEntry): string { + const message = Array.isArray(reportAction?.message) ? reportAction?.message.at(0) : reportAction?.message; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const text = stripFollowupListFromHtml(message?.html) || (message?.text ?? ''); + return text ? Parser.htmlToText(text) : ''; +} + +function getSpendRuleActionVerb(translate: LocalizedTranslate, action: string): string { + if (action === CONST.SPEND_RULES.ACTION.BLOCK) { + return translate('workspaceActions.expensifyCardRule.actionVerb.block'); + } + if (action === CONST.SPEND_RULES.ACTION.ALLOW) { + return translate('workspaceActions.expensifyCardRule.actionVerb.allow'); + } + return ''; +} + +function getSpendRuleAmountOperatorWord(translate: LocalizedTranslate, operator: string): string { + if (operator === CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO) { + return translate('workspaceActions.expensifyCardRule.amountOperator.under'); + } + if (operator === CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN) { + return translate('workspaceActions.expensifyCardRule.amountOperator.over'); + } + return ''; +} + +function getSpendRuleAmountString(translate: LocalizedTranslate, amount: {operator: string; value: string[]}, currency: string): string { + const operatorWord = getSpendRuleAmountOperatorWord(translate, amount.operator); + if (amount.value.length === 0) { + return ''; + } + return translate('workspaceActions.expensifyCardRule.amountFilter', {operator: operatorWord, amount: formatSpendRuleAmount(amount, currency)}); +} + +function getSpendRuleCardsSummary(translate: LocalizedTranslate, cards: ReadonlyArray<{displayName?: string}> | undefined): string { + if (!cards || cards.length === 0) { + return translate('workspaceActions.expensifyCardRule.theCard'); + } + if (cards.length === 1) { + const displayName = cards.at(0)?.displayName ?? ''; + return displayName !== '' ? displayName : translate('workspaceActions.expensifyCardRule.theCard'); + } + return translate('workspaceActions.expensifyCardRule.multipleCards', {count: cards.length}); +} + +function getSpendRuleJoinFilters(translate: LocalizedTranslate, items: readonly string[]): string { + const filtered = items.filter((value) => typeof value === 'string' && value !== ''); + return translate('workspaceActions.expensifyCardRule.joinFilters', {items: filtered}); +} + +function getSpendRuleCategoryDisplayName(translate: LocalizedTranslate, category: string): string { + const knownCategories = Object.values(CONST.SPEND_RULES.CATEGORIES) as string[]; + if (knownCategories.includes(category)) { + return translate(`workspace.rules.spendRules.categoryOptions.${category as ValueOf}`); + } + return category; +} + +function getSpendRuleRestrictionVerb(translate: LocalizedTranslate, action: string): string { + if (action === CONST.SPEND_RULES.ACTION.BLOCK) { + return translate('workspaceActions.expensifyCardRule.restrictionVerb.block'); + } + if (action === CONST.SPEND_RULES.ACTION.ALLOW) { + return translate('workspaceActions.expensifyCardRule.restrictionVerb.allow'); + } + return action; +} + +function formatSpendRuleAmount(amount: {value: string[]}, currency: string): string { + return convertAmountToDisplayString(getSpendRuleValueInCents(amount.value), currency); +} + +type SpendRuleStringDiff = {added: string[]; removed: string[]}; + +function computeSpendRuleStringDiff(oldValues: string[], newValues: string[]): SpendRuleStringDiff { + const oldSet = Array.from(new Set(oldValues)); + const newSet = Array.from(new Set(newValues)); + const added = newSet.filter((value) => !oldSet.includes(value)).sort(); + const removed = oldSet.filter((value) => !newSet.includes(value)).sort(); + return {added, removed}; +} + +type SpendRuleAmount = {operator: string; value: string[]}; +type SpendRuleAmountDiff = {added: SpendRuleAmount[]; removed: SpendRuleAmount[]}; + +function getSpendRuleValueInCents(value: string[]): number { + const firstValue = value.at(0) ?? ''; + return firstValue !== '' && Number.isFinite(Number(firstValue)) ? Math.round(parseFloat(firstValue) * 100) : 0; +} + +function computeSpendRuleAmountDiff(oldAmounts: SpendRuleAmount[], newAmounts: SpendRuleAmount[]): SpendRuleAmountDiff { + const oldAmount = oldAmounts.at(0); + const newAmount = newAmounts.at(0); + if (!oldAmount && !newAmount) { + return {added: [], removed: []}; + } + if (!oldAmount) { + return {added: newAmount ? [newAmount] : [], removed: []}; + } + if (!newAmount) { + return {added: [], removed: [oldAmount]}; + } + const sameAmount = getSpendRuleValueInCents(oldAmount.value) === getSpendRuleValueInCents(newAmount.value); + if (sameAmount) { + return {added: [], removed: []}; + } + return { + added: [newAmount], + removed: [oldAmount], + }; +} + +type SpendRuleCard = {cardID?: number | string; displayName?: string}; +type SpendRuleCardDiff = {added: SpendRuleCard[]; removed: SpendRuleCard[]}; + +function getSpendRuleCardID(card: SpendRuleCard): number | undefined { + const cardID = card?.cardID; + if (typeof cardID === 'number' && Number.isFinite(cardID)) { + return cardID; + } + if (typeof cardID === 'string' && /^\d+$/.test(cardID)) { + return Number.parseInt(cardID, 10); + } + return undefined; +} + +function computeSpendRuleCardDiff(oldCards: SpendRuleCard[], newCards: SpendRuleCard[]): SpendRuleCardDiff { + const oldByID = new Map(); + for (const card of oldCards) { + const id = getSpendRuleCardID(card); + if (id !== undefined) { + oldByID.set(id, card); + } + } + const newByID = new Map(); + for (const card of newCards) { + const id = getSpendRuleCardID(card); + if (id !== undefined) { + newByID.set(id, card); + } + } + const added: SpendRuleCard[] = []; + for (const [id, card] of newByID) { + if (!oldByID.has(id)) { + added.push(card); + } + } + const removed: SpendRuleCard[] = []; + for (const [id, card] of oldByID) { + if (!newByID.has(id)) { + removed.push(card); + } + } + return {added, removed}; +} + +type SpendRulePhraseVerb = 'added' | 'removed' | 'changed' | 'set' | 'applied'; +type SpendRulePhraseAdjective = '' | typeof CONST.SPEND_RULES.ACTION.BLOCK | typeof CONST.SPEND_RULES.ACTION.ALLOW; + +type SpendRulePhrase = { + verb: SpendRulePhraseVerb; + adjective: SpendRulePhraseAdjective; + bodyWithAdjective: string; + bodyWithoutAdjective: string; +}; + +function getSpendRulePhraseVerbWord(translate: LocalizedTranslate, verb: SpendRulePhraseVerb): string { + return translate(`workspaceActions.expensifyCardRule.update.phraseVerb.${verb}`); +} + +function joinSpendRulePhrases(translate: LocalizedTranslate, phrases: readonly SpendRulePhrase[]): string { + if (phrases.length === 0) { + return ''; + } + if (phrases.length === 1) { + const phrase = phrases.at(0); + if (!phrase) { + return ''; + } + return `${getSpendRulePhraseVerbWord(translate, phrase.verb)} ${phrase.bodyWithAdjective}`; + } + + const firstVerb = phrases.at(0)?.verb; + const allSameVerb = firstVerb !== undefined && phrases.every((phrase) => phrase.verb === firstVerb); + + if (!allSameVerb) { + const parts = phrases.map((phrase) => `${getSpendRulePhraseVerbWord(translate, phrase.verb)} ${phrase.bodyWithAdjective}`); + return getSpendRuleJoinFilters(translate, parts); + } + + const firstPhrase = phrases.at(0); + if (!firstPhrase) { + return ''; + } + const firstAdjective = firstPhrase.adjective; + const parts: string[] = [`${getSpendRulePhraseVerbWord(translate, firstPhrase.verb)} ${firstPhrase.bodyWithAdjective}`]; + for (let i = 1; i < phrases.length; i++) { + const phrase = phrases.at(i); + if (!phrase) { + continue; + } + const useOwnAdjective = phrase.adjective !== '' && phrase.adjective !== firstAdjective; + parts.push(useOwnAdjective ? phrase.bodyWithAdjective : phrase.bodyWithoutAdjective); + } + return getSpendRuleJoinFilters(translate, parts); +} + +function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { + if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE)) { + return ''; + } + const message = getOriginalMessage(reportAction) ?? {}; + const action = message.action ?? CONST.SPEND_RULES.ACTION.ALLOW; + const currency = message.currency ?? CONST.CURRENCY.USD; + const merchants = message.merchants ?? []; + const categories = message.categories ?? []; + const amounts = message.amounts ?? []; + const cards = message.cards ?? []; + + const items: string[] = []; + for (const merchant of merchants) { + items.push(`'${merchant}'`); + } + for (const category of categories) { + items.push(`'${getSpendRuleCategoryDisplayName(translate, category)}'`); + } + for (const amount of amounts) { + const formattedAmount = getSpendRuleAmountString(translate, amount, currency); + if (formattedAmount !== '') { + items.push(formattedAmount); + } + } + + const verb = getSpendRuleActionVerb(translate, action); + const filters = getSpendRuleJoinFilters(translate, items); + const cardsSummary = getSpendRuleCardsSummary(translate, cards); + + if (verb === '') { + return getSpendRuleFallbackReportActionText(reportAction); + } + + return translate('workspaceActions.expensifyCardRule.addRule', {verb, filters, cards: cardsSummary}); +} + +function pushDiffPhrases( + phrases: SpendRulePhrase[], + diff: SpendRuleStringDiff, + adjective: SpendRulePhraseAdjective, + adjectiveWord: string, + getDisplayName: (value: string) => string, + formatBody: (params: {adjective: string; value: string}) => string, + formatBodyChange: (params: {adjective: string; oldValue: string; newValue: string}) => string, +): void { + if (diff.added.length === 1 && diff.removed.length === 1) { + const oldValue = getDisplayName(diff.removed.at(0) ?? ''); + const newValue = getDisplayName(diff.added.at(0) ?? ''); + phrases.push({ + verb: 'changed', + adjective, + bodyWithAdjective: formatBodyChange({adjective: adjectiveWord, oldValue, newValue}), + bodyWithoutAdjective: formatBodyChange({adjective: '', oldValue, newValue}), + }); + } else { + for (const value of diff.added) { + const display = getDisplayName(value); + phrases.push({ + verb: 'added', + adjective, + bodyWithAdjective: formatBody({adjective: adjectiveWord, value: display}), + bodyWithoutAdjective: formatBody({adjective: '', value: display}), + }); + } + for (const value of diff.removed) { + const display = getDisplayName(value); + phrases.push({ + verb: 'removed', + adjective, + bodyWithAdjective: formatBody({adjective: adjectiveWord, value: display}), + bodyWithoutAdjective: formatBody({adjective: '', value: display}), + }); + } + } +} + +function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { + if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE)) { + return ''; + } + const message = getOriginalMessage(reportAction) ?? {}; + const oldAction = message.oldAction ?? CONST.SPEND_RULES.ACTION.ALLOW; + const newAction = message.action ?? CONST.SPEND_RULES.ACTION.ALLOW; + const actionChanged = oldAction !== '' && oldAction !== newAction; + const currency = message.currency ?? CONST.CURRENCY.USD; + + const oldMerchants = message.oldMerchants ?? []; + const newMerchants = message.merchants ?? []; + const oldCategories = message.oldCategories ?? []; + const newCategories = message.categories ?? []; + const oldAmounts = message.oldAmounts ?? []; + const newAmounts = message.amounts ?? []; + const oldCards = message.oldCards ?? []; + const newCards = message.cards ?? []; + + const merchantDiff = computeSpendRuleStringDiff(oldMerchants, newMerchants); + const categoryDiff = computeSpendRuleStringDiff(oldCategories, newCategories); + const amountDiff = computeSpendRuleAmountDiff(oldAmounts, newAmounts); + const cardDiff = computeSpendRuleCardDiff(oldCards, newCards); + + const merchantsChanged = merchantDiff.added.length > 0 || merchantDiff.removed.length > 0; + const categoriesChanged = categoryDiff.added.length > 0 || categoryDiff.removed.length > 0; + const amountsChanged = amountDiff.added.length > 0 || amountDiff.removed.length > 0; + const cardsChanged = cardDiff.added.length > 0 || cardDiff.removed.length > 0; + const filtersAndCardsUnchanged = !merchantsChanged && !categoriesChanged && !amountsChanged && !cardsChanged; + + const newCardsSummary = getSpendRuleCardsSummary(translate, newCards); + + if (actionChanged && filtersAndCardsUnchanged) { + return translate('workspaceActions.expensifyCardRule.update.modeChange', { + fromAction: getSpendRuleRestrictionVerb(translate, oldAction), + toAction: getSpendRuleRestrictionVerb(translate, newAction), + cards: newCardsSummary, + }); + } + + if (cardsChanged && !merchantsChanged && !categoriesChanged && !amountsChanged && !actionChanged) { + if (cardDiff.added.length > 0 && cardDiff.removed.length === 0) { + return translate('workspaceActions.expensifyCardRule.update.appliedToAdditionalCards', {count: cardDiff.added.length}); + } + if (cardDiff.added.length === 0 && cardDiff.removed.length > 0) { + return translate('workspaceActions.expensifyCardRule.removeRule', {cards: getSpendRuleCardsSummary(translate, cardDiff.removed)}); + } + } + + const adjective: SpendRulePhraseAdjective = newAction === CONST.SPEND_RULES.ACTION.BLOCK || newAction === CONST.SPEND_RULES.ACTION.ALLOW ? newAction : ''; + const adjectiveWord = getSpendRuleActionVerb(translate, adjective); + const phrases: SpendRulePhrase[] = []; + + pushDiffPhrases( + phrases, + merchantDiff, + adjective, + adjectiveWord, + (value) => value, + (params) => translate('workspaceActions.expensifyCardRule.update.bodyMerchant', params), + (params) => translate('workspaceActions.expensifyCardRule.update.bodyMerchantChange', params), + ); + pushDiffPhrases( + phrases, + categoryDiff, + adjective, + adjectiveWord, + (category) => getSpendRuleCategoryDisplayName(translate, category), + (params) => translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', params), + (params) => translate('workspaceActions.expensifyCardRule.update.bodySpendCategoryChange', params), + ); + + if (amountDiff.added.length === 1 && amountDiff.removed.length === 1) { + const oldValue = formatSpendRuleAmount(amountDiff.removed.at(0) ?? {value: []}, currency); + const newValue = formatSpendRuleAmount(amountDiff.added.at(0) ?? {value: []}, currency); + const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountChange', {oldValue, newValue}); + phrases.push({verb: 'changed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); + } else { + for (const amount of amountDiff.added) { + const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountSet', {value: formatSpendRuleAmount(amount, currency)}); + phrases.push({verb: 'set', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); + } + if (amountDiff.removed.length > 0) { + const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmount'); + const removedPhrase: SpendRulePhrase = {verb: 'removed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}; + phrases.push(...Array.from({length: amountDiff.removed.length}).fill(removedPhrase)); + } + } + + if (cardDiff.added.length > 0) { + const body = translate('workspaceActions.expensifyCardRule.update.bodyAppliedToAdditionalCards', {count: cardDiff.added.length}); + phrases.push({verb: 'applied', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); + } + if (cardDiff.removed.length > 0) { + const body = translate('workspaceActions.expensifyCardRule.update.bodyRemovedFromCards', {cards: getSpendRuleCardsSummary(translate, cardDiff.removed)}); + phrases.push({verb: 'removed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); + } + + if (phrases.length === 0) { + return getSpendRuleFallbackReportActionText(reportAction); + } + + const joined = joinSpendRulePhrases(translate, phrases); + + if (cardsChanged) { + return joined; + } + + const onlyRemovedPhrase = phrases.length === 1 && phrases.at(0)?.verb === 'removed'; + if (onlyRemovedPhrase) { + return translate('workspaceActions.expensifyCardRule.update.composeFromCards', {content: joined, cards: newCardsSummary}); + } + + return translate('workspaceActions.expensifyCardRule.update.composeOnCards', {content: joined, cards: newCardsSummary}); +} + +function getRemoveExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { + if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE)) { + return ''; + } + const message = getOriginalMessage(reportAction) ?? {}; + const cards = message.cards ?? []; + const cardsSummary = getSpendRuleCardsSummary(translate, cards); + return translate('workspaceActions.expensifyCardRule.removeRule', {cards: cardsSummary}); +} + +export {getAddExpensifyCardRuleMessage, getRemoveExpensifyCardRuleMessage, getUpdateExpensifyCardRuleMessage}; diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 18ca5ad35a0f..9f6394f4e198 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -14,7 +14,6 @@ import CONST from '../../src/CONST'; import * as ReportActionsUtils from '../../src/libs/ReportActionsUtils'; import { getAddedCardFeedMessage, - getAddExpensifyCardRuleMessage, getAssignedCompanyCardMessage, getAutoPayApprovedReportsEnabledMessage, getAutoReimbursementMessage, @@ -33,8 +32,6 @@ import { getModerationFlagState, getOneTransactionThreadReportID, getOriginalMessage, - getRemoveExpensifyCardRuleMessage, - getUpdateExpensifyCardRuleMessage, getPolicyChangeLogMaxExpenseAgeMessage, getPolicyChangeLogMaxExpenseAmountMessage, getPolicyChangeLogMaxExpenseAmountNoItemizedReceiptMessage, @@ -5397,306 +5394,4 @@ describe('ReportActionsUtils', () => { expect(getModerationFlagState(action)).toEqual({latestDecision: undefined, hasBeenFlagged: false}); }); }); - - describe('getAddExpensifyCardRuleMessage', () => { - it('returns empty string for wrong action type', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CARD_FEED, - reportActionID: '1', - created: '', - originalMessage: {}, - } as ReportAction; - expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe(''); - }); - - it('returns allow message with no filters and no named card', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - action: CONST.SPEND_RULES.ACTION.ALLOW, - cards: [], - }, - } as ReportAction; - expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe('allowed on the card'); - }); - - it('returns allow message with single named card and no filters', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - action: CONST.SPEND_RULES.ACTION.ALLOW, - cards: [{cardID: 1, displayName: 'My Visa'}], - }, - } as ReportAction; - expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe('allowed on My Visa'); - }); - - it('returns block message with merchant filter', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - action: CONST.SPEND_RULES.ACTION.BLOCK, - merchants: ['Starbucks'], - cards: [{cardID: 1, displayName: 'My Visa'}], - }, - } as ReportAction; - expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("blocked 'Starbucks' on My Visa"); - }); - - it('returns allow message with category filter', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - action: CONST.SPEND_RULES.ACTION.ALLOW, - categories: [CONST.SPEND_RULES.CATEGORIES.DINING], - cards: [{cardID: 1, displayName: 'My Visa'}], - }, - } as ReportAction; - expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("allowed 'Dining' on My Visa"); - }); - - it('returns block message with amount-over filter', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - action: CONST.SPEND_RULES.ACTION.BLOCK, - currency: CONST.CURRENCY.USD, - amounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN, value: ['100.00']}], - cards: [{cardID: 1, displayName: 'My Visa'}], - }, - } as ReportAction; - expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe('blocked amounts over $100.00 on My Visa'); - }); - - it('returns allow message with merchant and amount-under filter joined', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - action: CONST.SPEND_RULES.ACTION.ALLOW, - currency: CONST.CURRENCY.USD, - merchants: ['Amazon'], - amounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO, value: ['50.00']}], - cards: [{cardID: 1, displayName: 'My Visa'}], - }, - } as ReportAction; - expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("allowed 'Amazon' and amounts under $50.00 on My Visa"); - }); - - it('returns message with multiple-cards summary', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - action: CONST.SPEND_RULES.ACTION.ALLOW, - cards: [ - {cardID: 1, displayName: 'Card A'}, - {cardID: 2, displayName: 'Card B'}, - ], - }, - } as ReportAction; - expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe('allowed on 2 cards'); - }); - }); - - describe('getUpdateExpensifyCardRuleMessage', () => { - it('returns empty string for wrong action type', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: {}, - } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe(''); - }); - - it('returns mode-change message when only action changed', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - oldAction: CONST.SPEND_RULES.ACTION.ALLOW, - action: CONST.SPEND_RULES.ACTION.BLOCK, - cards: [{cardID: 1, displayName: 'My Visa'}], - }, - } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('changed spend rule from only allow to block on My Visa'); - }); - - it('returns applied-to-additional-cards message when only new cards were added', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - action: CONST.SPEND_RULES.ACTION.ALLOW, - oldCards: [{cardID: 1, displayName: 'Card A'}], - cards: [ - {cardID: 1, displayName: 'Card A'}, - {cardID: 2, displayName: 'Card B'}, - ], - }, - } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('applied spend rule to 1 additional cards'); - }); - - it('returns remove-rule message when only cards were removed', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - action: CONST.SPEND_RULES.ACTION.ALLOW, - oldCards: [ - {cardID: 1, displayName: 'Card A'}, - {cardID: 2, displayName: 'Card B'}, - ], - cards: [{cardID: 1, displayName: 'Card A'}], - }, - } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('removed spend rule from Card B'); - }); - - it('returns added-merchant message with adjective for allow action', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - action: CONST.SPEND_RULES.ACTION.ALLOW, - oldMerchants: [], - merchants: ['Starbucks'], - cards: [{cardID: 1, displayName: 'My Visa'}], - }, - } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe("added allowed merchant 'Starbucks' on My Visa"); - }); - - it('returns changed-category message when single category was swapped', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - action: CONST.SPEND_RULES.ACTION.BLOCK, - oldCategories: [CONST.SPEND_RULES.CATEGORIES.AIRLINES], - categories: [CONST.SPEND_RULES.CATEGORIES.DINING], - cards: [{cardID: 1, displayName: 'My Visa'}], - }, - } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe("changed blocked spend category from 'Airlines' to 'Dining' on My Visa"); - }); - - it('returns set-max-amount message when amount is newly added', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - action: CONST.SPEND_RULES.ACTION.BLOCK, - currency: CONST.CURRENCY.USD, - oldAmounts: [], - amounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN, value: ['100.00']}], - cards: [{cardID: 1, displayName: 'My Visa'}], - }, - } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('set max amount to $100.00 on My Visa'); - }); - - it('returns changed-max-amount message when amount value changes', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - action: CONST.SPEND_RULES.ACTION.BLOCK, - currency: CONST.CURRENCY.USD, - oldAmounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN, value: ['50.00']}], - amounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN, value: ['100.00']}], - cards: [{cardID: 1, displayName: 'My Visa'}], - }, - } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('changed max amount from $50.00 to $100.00 on My Visa'); - }); - - it('returns removed-max-amount message when amount is cleared', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - action: CONST.SPEND_RULES.ACTION.BLOCK, - currency: CONST.CURRENCY.USD, - oldAmounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN, value: ['100.00']}], - amounts: [], - cards: [{cardID: 1, displayName: 'My Visa'}], - }, - } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('removed max amount from My Visa'); - }); - }); - - describe('getRemoveExpensifyCardRuleMessage', () => { - it('returns empty string for wrong action type', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: {}, - } as ReportAction; - expect(getRemoveExpensifyCardRuleMessage(translateLocal, action)).toBe(''); - }); - - it('returns message with single named card', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - cards: [{cardID: 1, displayName: 'My Visa'}], - }, - } as ReportAction; - expect(getRemoveExpensifyCardRuleMessage(translateLocal, action)).toBe('removed spend rule from My Visa'); - }); - - it('returns message with multiple-cards summary', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - cards: [ - {cardID: 1, displayName: 'Card A'}, - {cardID: 2, displayName: 'Card B'}, - ], - }, - } as ReportAction; - expect(getRemoveExpensifyCardRuleMessage(translateLocal, action)).toBe('removed spend rule from 2 cards'); - }); - - it('returns message with fallback "the card" when cards list is empty', () => { - const action = { - actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE, - reportActionID: '1', - created: '', - originalMessage: { - cards: [], - }, - } as ReportAction; - expect(getRemoveExpensifyCardRuleMessage(translateLocal, action)).toBe('removed spend rule from the card'); - }); - }); }); diff --git a/tests/unit/SpendRuleChangeLogUtilsTest.ts b/tests/unit/SpendRuleChangeLogUtilsTest.ts new file mode 100644 index 000000000000..772c191ef696 --- /dev/null +++ b/tests/unit/SpendRuleChangeLogUtilsTest.ts @@ -0,0 +1,321 @@ +import {getAddExpensifyCardRuleMessage, getRemoveExpensifyCardRuleMessage, getUpdateExpensifyCardRuleMessage} from '@libs/SpendRuleChangeLogUtils'; +import CONST from '@src/CONST'; +import IntlStore from '@src/languages/IntlStore'; +import type {ReportAction} from '@src/types/onyx'; +import {translateLocal} from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +describe('SpendRuleChangeLogUtils', () => { + beforeAll(() => { + IntlStore.load(CONST.LOCALES.DEFAULT); + return waitForBatchedUpdates(); + }); + + describe('getAddExpensifyCardRuleMessage', () => { + it('returns empty string for wrong action type', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CARD_FEED, + reportActionID: '1', + created: '', + originalMessage: {}, + } as ReportAction; + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe(''); + }); + + it('returns allow message with no filters and no named card', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.ALLOW, + cards: [], + }, + } as ReportAction; + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe('allowed on the card'); + }); + + it('returns allow message with single named card and no filters', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.ALLOW, + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe('allowed on My Visa'); + }); + + it('returns block message with merchant filter', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.BLOCK, + merchants: ['Starbucks'], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("blocked 'Starbucks' on My Visa"); + }); + + it('returns allow message with category filter', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.ALLOW, + categories: [CONST.SPEND_RULES.CATEGORIES.DINING], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("allowed 'Dining' on My Visa"); + }); + + it('returns block message with amount-over filter', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.BLOCK, + currency: CONST.CURRENCY.USD, + amounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN, value: ['100.00']}], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe('blocked amounts over $100.00 on My Visa'); + }); + + it('returns allow message with merchant and amount-under filter joined', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.ALLOW, + currency: CONST.CURRENCY.USD, + merchants: ['Amazon'], + amounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO, value: ['50.00']}], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("allowed 'Amazon' and amounts under $50.00 on My Visa"); + }); + + it('returns message with multiple-cards summary', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.ALLOW, + cards: [ + {cardID: 1, displayName: 'Card A'}, + {cardID: 2, displayName: 'Card B'}, + ], + }, + } as ReportAction; + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe('allowed on 2 cards'); + }); + }); + + describe('getUpdateExpensifyCardRuleMessage', () => { + it('returns empty string for wrong action type', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: {}, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe(''); + }); + + it('returns mode-change message when only action changed', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + oldAction: CONST.SPEND_RULES.ACTION.ALLOW, + action: CONST.SPEND_RULES.ACTION.BLOCK, + oldCards: [{cardID: 1, displayName: 'My Visa'}], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('changed spend rule from only allow to block on My Visa'); + }); + + it('returns applied-to-additional-cards message when only new cards were added', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.ALLOW, + oldCards: [{cardID: 1, displayName: 'Card A'}], + cards: [ + {cardID: 1, displayName: 'Card A'}, + {cardID: 2, displayName: 'Card B'}, + ], + }, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('applied spend rule to 1 additional cards'); + }); + + it('returns remove-rule message when only cards were removed', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.ALLOW, + oldCards: [ + {cardID: 1, displayName: 'Card A'}, + {cardID: 2, displayName: 'Card B'}, + ], + cards: [{cardID: 1, displayName: 'Card A'}], + }, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('removed spend rule from Card B'); + }); + + it('returns added-merchant message with adjective for allow action', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.ALLOW, + oldMerchants: [], + merchants: ['Starbucks'], + oldCards: [{cardID: 1, displayName: 'My Visa'}], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe("added allowed merchant 'Starbucks' on My Visa"); + }); + + it('returns changed-category message when single category was swapped', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.BLOCK, + oldCategories: [CONST.SPEND_RULES.CATEGORIES.AIRLINES], + categories: [CONST.SPEND_RULES.CATEGORIES.DINING], + oldCards: [{cardID: 1, displayName: 'My Visa'}], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe("changed blocked spend category from 'Airlines' to 'Dining' on My Visa"); + }); + + it('returns set-max-amount message when amount is newly added', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.BLOCK, + currency: CONST.CURRENCY.USD, + oldAmounts: [], + amounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN, value: ['100.00']}], + oldCards: [{cardID: 1, displayName: 'My Visa'}], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('set max amount to $100.00 on My Visa'); + }); + + it('returns changed-max-amount message when amount value changes', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.BLOCK, + currency: CONST.CURRENCY.USD, + oldAmounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN, value: ['50.00']}], + amounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN, value: ['100.00']}], + oldCards: [{cardID: 1, displayName: 'My Visa'}], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('changed max amount from $50.00 to $100.00 on My Visa'); + }); + + it('returns removed-max-amount message when amount is cleared', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + action: CONST.SPEND_RULES.ACTION.BLOCK, + currency: CONST.CURRENCY.USD, + oldAmounts: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN, value: ['100.00']}], + amounts: [], + oldCards: [{cardID: 1, displayName: 'My Visa'}], + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('removed max amount from My Visa'); + }); + }); + + describe('getRemoveExpensifyCardRuleMessage', () => { + it('returns empty string for wrong action type', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: {}, + } as ReportAction; + expect(getRemoveExpensifyCardRuleMessage(translateLocal, action)).toBe(''); + }); + + it('returns message with single named card', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + cards: [{cardID: 1, displayName: 'My Visa'}], + }, + } as ReportAction; + expect(getRemoveExpensifyCardRuleMessage(translateLocal, action)).toBe('removed spend rule from My Visa'); + }); + + it('returns message with multiple-cards summary', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + cards: [ + {cardID: 1, displayName: 'Card A'}, + {cardID: 2, displayName: 'Card B'}, + ], + }, + } as ReportAction; + expect(getRemoveExpensifyCardRuleMessage(translateLocal, action)).toBe('removed spend rule from 2 cards'); + }); + + it('returns message with fallback "the card" when cards list is empty', () => { + const action = { + actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE, + reportActionID: '1', + created: '', + originalMessage: { + cards: [], + }, + } as ReportAction; + expect(getRemoveExpensifyCardRuleMessage(translateLocal, action)).toBe('removed spend rule from the card'); + }); + }); +}); From fcd3de03b1d94ccdb488991598ea3c9134b711cd Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 26 May 2026 12:37:00 -0600 Subject: [PATCH 28/43] finish refactoring new util file --- src/libs/ReportActionsUtils.ts | 2 -- src/libs/SidebarUtils.ts | 4 +--- src/libs/SpendRuleChangeLogUtils.ts | 16 +--------------- .../report/ContextMenu/ContextMenuActions.tsx | 4 +--- .../actionContents/PolicyChangeLogContent.tsx | 4 +--- 5 files changed, 4 insertions(+), 26 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 34b7b61e21da..3df89dc42754 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -4569,8 +4569,6 @@ function hasReasoning(action: OnyxInputOrEntry): boolean { return !!originalMessage && typeof originalMessage === 'object' && 'reasoning' in originalMessage && !!originalMessage.reasoning; } -export {getAddExpensifyCardRuleMessage, getRemoveExpensifyCardRuleMessage, getUpdateExpensifyCardRuleMessage} from './SpendRuleChangeLogUtils'; - export { doesReportHaveVisibleActions, extractLinksFromMessageHtml, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 9d6f84e2f2d0..9044dbf84a63 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -42,7 +42,6 @@ import { getAddedBudgetMessage, getAddedCardFeedMessage, getAddedConnectionMessage, - getAddExpensifyCardRuleMessage, getAssignedCompanyCardMessage, getAutoPayApprovedReportsEnabledMessage, getAutoReimbursementMessage, @@ -79,7 +78,6 @@ import { getReimburserUpdateMessage, getRemovedCardFeedMessage, getRemovedConnectionMessage, - getRemoveExpensifyCardRuleMessage, getRenamedAction, getRenamedCardFeedMessage, getReportAction, @@ -111,7 +109,6 @@ import { getUpdatedSharedBudgetNotificationMessage, getUpdatedTimeEnabledMessage, getUpdatedTimeRateMessage, - getUpdateExpensifyCardRuleMessage, getUpdateRoomDescriptionMessage, getWorkspaceAttendeeTrackingUpdateMessage, getWorkspaceCategoriesUpdatedMessage, @@ -198,6 +195,7 @@ import { shouldReportBeInOptionList, shouldReportShowSubscript, } from './ReportUtils'; +import {getAddExpensifyCardRuleMessage, getRemoveExpensifyCardRuleMessage, getUpdateExpensifyCardRuleMessage} from './SpendRuleChangeLogUtils'; import StringUtils from './StringUtils'; import {getTaskReportActionMessage} from './TaskUtils'; diff --git a/src/libs/SpendRuleChangeLogUtils.ts b/src/libs/SpendRuleChangeLogUtils.ts index 6cb1383e23c6..30efa5d62090 100644 --- a/src/libs/SpendRuleChangeLogUtils.ts +++ b/src/libs/SpendRuleChangeLogUtils.ts @@ -3,24 +3,10 @@ import type {ValueOf} from 'type-fest'; import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; import type ReportAction from '@src/types/onyx/ReportAction'; -import type {OriginalMessage} from '@src/types/onyx/ReportAction'; -import type ReportActionName from '@src/types/onyx/ReportActionName'; import {convertAmountToDisplayString} from './CurrencyUtils'; import Parser from './Parser'; import stripFollowupListFromHtml from './ReportActionFollowupUtils/stripFollowupListFromHtml'; - -function isActionOfType(action: OnyxEntry, actionName: T): action is ReportAction { - return action?.actionName === actionName; -} - -function getOriginalMessage(reportAction: OnyxEntry>): OriginalMessage | undefined { - const candidate = !Array.isArray(reportAction?.message) ? (reportAction?.message ?? reportAction?.originalMessage) : reportAction?.originalMessage; - - if (candidate === null || typeof candidate !== 'object') { - return undefined; - } - return candidate as OriginalMessage; -} +import {getOriginalMessage, isActionOfType} from './ReportActionsUtils'; function getSpendRuleFallbackReportActionText(reportAction: OnyxEntry): string { const message = Array.isArray(reportAction?.message) ? reportAction?.message.at(0) : reportAction?.message; diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx index e8e053f52b4f..cdea4cfde232 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx @@ -33,7 +33,6 @@ import { getAddedBudgetMessage, getAddedCardFeedMessage, getAddedConnectionMessage, - getAddExpensifyCardRuleMessage, getAssignedCompanyCardMessage, getAutoPayApprovedReportsEnabledMessage, getAutoReimbursementMessage, @@ -79,7 +78,6 @@ import { getReimburserUpdateMessage, getRemovedCardFeedMessage, getRemovedConnectionMessage, - getRemoveExpensifyCardRuleMessage, getRenamedAction, getRenamedCardFeedMessage, getReportAction, @@ -110,7 +108,6 @@ import { getUpdatedSharedBudgetNotificationMessage, getUpdatedTimeEnabledMessage, getUpdatedTimeRateMessage, - getUpdateExpensifyCardRuleMessage, getUpdateRoomDescriptionMessage, getWorkspaceAttendeeTrackingUpdateMessage, getWorkspaceCategoriesUpdatedMessage, @@ -187,6 +184,7 @@ import { shouldDisplayThreadReplies as shouldDisplayThreadRepliesReportUtils, shouldShowMarkAsDone, } from '@libs/ReportUtils'; +import {getAddExpensifyCardRuleMessage, getRemoveExpensifyCardRuleMessage, getUpdateExpensifyCardRuleMessage} from '@libs/SpendRuleChangeLogUtils'; import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils'; import {setDownload} from '@userActions/Download'; import {toggleEmojiReaction} from '@userActions/EmojiReactions'; diff --git a/src/pages/inbox/report/actionContents/PolicyChangeLogContent.tsx b/src/pages/inbox/report/actionContents/PolicyChangeLogContent.tsx index 74145e3522fd..ced30612645f 100644 --- a/src/pages/inbox/report/actionContents/PolicyChangeLogContent.tsx +++ b/src/pages/inbox/report/actionContents/PolicyChangeLogContent.tsx @@ -10,7 +10,6 @@ import { getAddedBudgetMessage, getAddedCardFeedMessage, getAddedConnectionMessage, - getAddExpensifyCardRuleMessage, getAssignedCompanyCardMessage, getAutoPayApprovedReportsEnabledMessage, getAutoReimbursementMessage, @@ -37,7 +36,6 @@ import { getReimburserUpdateMessage, getRemovedCardFeedMessage, getRemovedConnectionMessage, - getRemoveExpensifyCardRuleMessage, getRenamedCardFeedMessage, getRequireCompanyCardsEnabledMessage, getSetAutoJoinMessage, @@ -62,7 +60,6 @@ import { getUpdatedSharedBudgetNotificationMessage, getUpdatedTimeEnabledMessage, getUpdatedTimeRateMessage, - getUpdateExpensifyCardRuleMessage, getWorkspaceAttendeeTrackingUpdateMessage, getWorkspaceCategoriesUpdatedMessage, getWorkspaceCategoryUpdateMessage, @@ -85,6 +82,7 @@ import { getWorkspaceUpdateFieldMessage, } from '@libs/ReportActionsUtils'; import {getWorkspaceNameUpdatedMessage} from '@libs/ReportUtils'; +import {getAddExpensifyCardRuleMessage, getRemoveExpensifyCardRuleMessage, getUpdateExpensifyCardRuleMessage} from '@libs/SpendRuleChangeLogUtils'; import ReportActionItemBasicMessage from '@pages/inbox/report/ReportActionItemBasicMessage'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; From b3553f7d43c133946dd93b7f50cc3a97aec56b82 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 26 May 2026 12:47:10 -0600 Subject: [PATCH 29/43] reafactor pushDiffs --- src/libs/SpendRuleChangeLogUtils.ts | 51 ++++++++++++++--------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/libs/SpendRuleChangeLogUtils.ts b/src/libs/SpendRuleChangeLogUtils.ts index 30efa5d62090..6a9eac1141c3 100644 --- a/src/libs/SpendRuleChangeLogUtils.ts +++ b/src/libs/SpendRuleChangeLogUtils.ts @@ -253,19 +253,19 @@ function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAct return translate('workspaceActions.expensifyCardRule.addRule', {verb, filters, cards: cardsSummary}); } -function pushDiffPhrases( - phrases: SpendRulePhrase[], +function getDiffPhrases( diff: SpendRuleStringDiff, adjective: SpendRulePhraseAdjective, adjectiveWord: string, getDisplayName: (value: string) => string, formatBody: (params: {adjective: string; value: string}) => string, formatBodyChange: (params: {adjective: string; oldValue: string; newValue: string}) => string, -): void { +): SpendRulePhrase[] { + const diffPhrases: SpendRulePhrase[] = []; if (diff.added.length === 1 && diff.removed.length === 1) { const oldValue = getDisplayName(diff.removed.at(0) ?? ''); const newValue = getDisplayName(diff.added.at(0) ?? ''); - phrases.push({ + diffPhrases.push({ verb: 'changed', adjective, bodyWithAdjective: formatBodyChange({adjective: adjectiveWord, oldValue, newValue}), @@ -274,7 +274,7 @@ function pushDiffPhrases( } else { for (const value of diff.added) { const display = getDisplayName(value); - phrases.push({ + diffPhrases.push({ verb: 'added', adjective, bodyWithAdjective: formatBody({adjective: adjectiveWord, value: display}), @@ -283,7 +283,7 @@ function pushDiffPhrases( } for (const value of diff.removed) { const display = getDisplayName(value); - phrases.push({ + diffPhrases.push({ verb: 'removed', adjective, bodyWithAdjective: formatBody({adjective: adjectiveWord, value: display}), @@ -291,6 +291,7 @@ function pushDiffPhrases( }); } } + return diffPhrases; } function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { @@ -344,26 +345,24 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report const adjective: SpendRulePhraseAdjective = newAction === CONST.SPEND_RULES.ACTION.BLOCK || newAction === CONST.SPEND_RULES.ACTION.ALLOW ? newAction : ''; const adjectiveWord = getSpendRuleActionVerb(translate, adjective); - const phrases: SpendRulePhrase[] = []; - - pushDiffPhrases( - phrases, - merchantDiff, - adjective, - adjectiveWord, - (value) => value, - (params) => translate('workspaceActions.expensifyCardRule.update.bodyMerchant', params), - (params) => translate('workspaceActions.expensifyCardRule.update.bodyMerchantChange', params), - ); - pushDiffPhrases( - phrases, - categoryDiff, - adjective, - adjectiveWord, - (category) => getSpendRuleCategoryDisplayName(translate, category), - (params) => translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', params), - (params) => translate('workspaceActions.expensifyCardRule.update.bodySpendCategoryChange', params), - ); + const phrases: SpendRulePhrase[] = [ + ...getDiffPhrases( + merchantDiff, + adjective, + adjectiveWord, + (value) => value, + (params) => translate('workspaceActions.expensifyCardRule.update.bodyMerchant', params), + (params) => translate('workspaceActions.expensifyCardRule.update.bodyMerchantChange', params), + ), + ...getDiffPhrases( + categoryDiff, + adjective, + adjectiveWord, + (category) => getSpendRuleCategoryDisplayName(translate, category), + (params) => translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', params), + (params) => translate('workspaceActions.expensifyCardRule.update.bodySpendCategoryChange', params), + ), + ]; if (amountDiff.added.length === 1 && amountDiff.removed.length === 1) { const oldValue = formatSpendRuleAmount(amountDiff.removed.at(0) ?? {value: []}, currency); From fa26ffe5eb55c3182eb156f7d99529a2dbce6180 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 26 May 2026 12:47:41 -0600 Subject: [PATCH 30/43] update copy --- src/languages/de.ts | 12 ++++++------ src/languages/es.ts | 9 ++++----- src/languages/fr.ts | 12 ++++++------ src/languages/it.ts | 10 +++++----- src/languages/ja.ts | 23 ++++++++++------------- src/languages/nl.ts | 14 +++++++------- src/languages/pl.ts | 7 +++---- src/languages/pt-BR.ts | 16 ++++++++-------- src/languages/zh-hans.ts | 30 +++++++++++++++--------------- 9 files changed, 64 insertions(+), 69 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 2987b9d3d6d0..91cec363be59 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7834,20 +7834,20 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc return text; }, removeRule: ({cards}: {cards: string}) => `Ausgaberegel von ${cards} entfernt`, - restrictionVerb: {block: 'Block', allow: 'nur zulassen'}, + restrictionVerb: {block: 'Block', allow: 'nur erlauben'}, update: { - modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `Ausgaberegel von ${fromAction} zu ${toAction} auf ${cards} geändert`, + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `Ausgaberegel von ${fromAction} auf ${toAction} auf ${cards} geändert`, appliedToAdditionalCards: ({count}: {count: number}) => `Ausgaberegel auf ${count} weitere Karten angewendet`, phraseVerb: {added: 'hinzugefügt', removed: 'entfernt', changed: 'geändert', set: 'festlegen', applied: 'angewendet'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} Händler „${value}“` : `Händler\\*in „${value}“`), + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} Händler '${value}'` : `Händler: '${value}'`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} Händler von „${oldValue}“ zu „${newValue}“` : `Händler von „${oldValue}“ zu „${newValue}“`, + adjective !== '' ? `${adjective} Händler von „${oldValue}“ in „${newValue}“` : `Händler von „${oldValue}“ zu „${newValue}“`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => adjective !== '' ? `${adjective} Ausgabenkategorie „${value}“` : `Ausgabenkategorie „${value}“`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} Ausgabenkategorie von „${oldValue}“ zu „${newValue}“` : `Ausgabenkategorie von „${oldValue}“ zu „${newValue}“`, + adjective !== '' ? `${adjective} Ausgabenkategorie von „${oldValue}“ in „${newValue}“` : `Ausgabenkategorie von „${oldValue}“ zu „${newValue}“`, bodyMaxAmount: 'Maximalbetrag', - bodyMaxAmountSet: ({value}: {value: string}) => `Maximalbetrag bis ${value}`, + bodyMaxAmountSet: ({value}: {value: string}) => `Maximalbetrag von ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `Maximalbetrag von ${oldValue} auf ${newValue}`, bodyAppliedToAdditionalCards: ({count}: {count: number}) => `Ausgabenregel auf ${count} zusätzliche Karten`, bodyRemovedFromCards: ({cards}: {cards: string}) => `Ausgaberegel von ${cards}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 993b2d0078b2..a5a3595cec08 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1732,7 +1732,6 @@ const translations: TranslationDeepObject = { } }, [CONST.NEXT_STEP.MESSAGE_KEY.WAITING_TO_MARK_AS_DONE]: (actor, actorType, _eta, _etaType) => { - // eslint-disable-next-line default-case switch (actorType) { case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER: return `Esperando a que lo marques como listo.`; @@ -7301,7 +7300,7 @@ ${amount} para ${merchant} - ${date}`, expensifyCardRule: { actionVerb: {block: 'bloqueado', allow: 'permitido'}, amountOperator: {over: 'encima', under: 'debajo'}, - amountFilter: ({operator, amount}: {operator: string; amount: string}) => `cantidades ${operator} ${amount}`, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `importes ${operator} ${amount}`, theCard: 'la tarjeta', multipleCards: ({count}: {count: number}) => `${count} tarjetas`, joinFilters: ({items}: {items: string[]}) => { @@ -7329,16 +7328,16 @@ ${amount} para ${merchant} - ${date}`, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `cambió la regla de gasto de ${fromAction} a ${toAction} en ${cards}`, appliedToAdditionalCards: ({count}: {count: number}) => `aplicó la regla de gasto a ${count} tarjetas adicionales`, - phraseVerb: {added: 'añadido', removed: 'eliminado', changed: 'cambiado', set: 'establecer', applied: 'aplicado'}, + phraseVerb: {added: 'añadió', removed: 'eliminado', changed: 'cambió', set: 'establecer', applied: 'aplicado'}, bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `Comercio ${adjective} «${value}»` : `comercio «${value}»`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} comercio de '${oldValue}' a '${newValue}'` : `comercio de «${oldValue}» a «${newValue}»`, + adjective !== '' ? `comercio ${adjective} de '${oldValue}' a '${newValue}'` : `comercio de '${oldValue}' a '${newValue}'`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => adjective !== '' ? `Categoría de gasto ${adjective} «${value}»` : `categoría de gasto «${value}»`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `Categoría de gasto ${adjective} de '${oldValue}' a '${newValue}'` : `categoría de gasto de «${oldValue}» a «${newValue}»`, bodyMaxAmount: 'importe máximo', - bodyMaxAmountSet: ({value}: {value: string}) => `importe máximo hasta ${value}`, + bodyMaxAmountSet: ({value}: {value: string}) => `importe máximo de ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `importe máximo de ${oldValue} a ${newValue}`, bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regla de gasto para ${count} tarjetas adicionales`, bodyRemovedFromCards: ({cards}: {cards: string}) => `regla de gasto de ${cards}`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index ea4157625045..8404e2156a55 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -851,7 +851,7 @@ const translations: TranslationDeepObject = { beginningOfChatHistory: (users: string) => `Cette discussion est avec ${users}.`, beginningOfChatHistoryPolicyExpenseChat: (workspaceName: string, submitterDisplayName: string) => `C’est ici que ${submitterDisplayName} soumettra des dépenses à ${workspaceName}. Utilisez simplement le bouton +.`, - beginningOfChatHistoryPolicyExpenseChatTrack: 'C\u2019est ici que vous suivrez vos dépenses', + beginningOfChatHistoryPolicyExpenseChatTrack: 'C’est ici que vous suivrez vos dépenses', beginningOfChatHistorySelfDM: 'Ceci est votre espace personnel. Utilisez-le pour vos notes, tâches, brouillons et rappels.', beginningOfChatHistorySystemDM: 'Bienvenue ! Procédons à la configuration.', chatWithAccountManager: 'Discutez avec votre gestionnaire de compte ici', @@ -7869,21 +7869,21 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `a modifié la règle de dépense de ${fromAction} à ${toAction} sur ${cards}`, appliedToAdditionalCards: ({count}: {count: number}) => `règle de dépense appliquée à ${count} cartes supplémentaires`, - phraseVerb: {added: 'ajouté', removed: 'supprimé', changed: 'modifié', set: 'définir', applied: 'appliqué'}, + phraseVerb: {added: 'ajouté', removed: 'Supprimé', changed: 'modifié', set: 'définir', applied: 'appliqué'}, bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `Commerçant ${adjective} « ${value} »` : `commerçant « ${value} »`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `Commerçant ${adjective} de « ${oldValue} » à « ${newValue} »` : `commerçant de « ${oldValue} » à « ${newValue} »`, + adjective !== '' ? `Commerçant ${adjective} de « ${oldValue} » à « ${newValue} »` : `commerçant de « ${oldValue} » à « ${newValue} »`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => - adjective !== '' ? `Catégorie de dépense ${adjective} « ${value} »` : `catégorie de dépense « ${value} »`, + adjective !== '' ? `Catégorie de dépenses ${adjective} « ${value} »` : `catégorie de dépense « ${value} »`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `Catégorie de dépense ${adjective} de « ${oldValue} » à « ${newValue} »` : `catégorie de dépense de « ${oldValue} » à « ${newValue} »`, bodyMaxAmount: 'montant maximal', - bodyMaxAmountSet: ({value}: {value: string}) => `montant maximum à ${value}`, + bodyMaxAmountSet: ({value}: {value: string}) => `montant maximal à ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `montant maximal de ${oldValue} à ${newValue}`, bodyAppliedToAdditionalCards: ({count}: {count: number}) => `règle de dépense pour ${count} cartes supplémentaires`, bodyRemovedFromCards: ({cards}: {cards: string}) => `règle de dépense à partir de ${cards}`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} sur ${cards}`, - composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} depuis ${cards}`, + composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} de ${cards}`, }, }, }, diff --git a/src/languages/it.ts b/src/languages/it.ts index 3cc53d7b2b76..1a4042be6853 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7799,7 +7799,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'abilitato' : 'disabilitato'} il requisito per gli acquisti con carta aziendale`, expensifyCardRule: { actionVerb: {block: 'bloccato', allow: 'consentito'}, - amountOperator: {over: 'terminato', under: 'sotto'}, + amountOperator: {over: 'oltre', under: 'sotto'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `importi ${operator} ${amount}`, theCard: 'la carta', multipleCards: ({count}: {count: number}) => `${count} carte`, @@ -7830,15 +7830,15 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, `ha modificato la regola di spesa da ${fromAction} a ${toAction} su ${cards}`, appliedToAdditionalCards: ({count}: {count: number}) => `regola di spesa applicata a ${count} carte aggiuntive`, phraseVerb: {added: 'aggiunto', removed: 'rimosso', changed: 'modificato', set: 'imposta', applied: 'applicata'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `Commerciante ${adjective} '${value}'` : `esercente '${value}'`), + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `commerciante ${adjective} '${value}'` : `esercente '${value}'`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `commerciante ${adjective} da '${oldValue}' a '${newValue}'` : `esercente da '${oldValue}' a '${newValue}'`, + adjective !== '' ? `esercente ${adjective} da '${oldValue}' a '${newValue}'` : `esercente da '${oldValue}' a '${newValue}'`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => - adjective !== '' ? `Categoria di spesa ${adjective} "${value}"` : `categoria di spesa "${value}"`, + adjective !== '' ? `Categoria di spesa ${adjective} '${value}'` : `categoria di spesa '${value}'`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `categoria di spesa ${adjective} da '${oldValue}' a '${newValue}'` : `categoria di spesa da '${oldValue}' a '${newValue}'`, bodyMaxAmount: 'importo massimo', - bodyMaxAmountSet: ({value}: {value: string}) => `importo massimo a ${value}`, + bodyMaxAmountSet: ({value}: {value: string}) => `importo massimo fino a ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `importo massimo da ${oldValue} a ${newValue}`, bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regola di spesa per ${count} carte aggiuntive`, bodyRemovedFromCards: ({cards}: {cards: string}) => `regola di spesa da ${cards}`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index d5c011428f4d..d4bb4aa73647 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7704,11 +7704,8 @@ ${reportName} `${fieldType}レポートフィールド「${fieldName}」を追加しました${defaultValue ? ` デフォルト値「${defaultValue}」付き` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? '有効' : '無効'} の法人カード購入要件`, expensifyCardRule: { - actionVerb: {block: 'ブロック済み', allow: '許可済み'}, - amountOperator: { - over: '上限', - under: '以下のいずれかで使われることが多いです。文脈に応じて変わります。\n\n- 〜の下に(位置・階層を表す場合)\n- 〜未満(数値・金額の場合)\n- 〜のもとで(条件・権限の場合)', - }, + actionVerb: {block: 'ブロック済み', allow: '許可されています'}, + amountOperator: {over: '終了', under: '以下のいずれかの意味に応じて使い分けますが、一般的なUI文脈では「以下」となることが多いです。'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `金額 ${operator} ${amount}`, theCard: 'カード', multipleCards: ({count}: {count: number}) => `${count} 枚のカード`, @@ -7729,7 +7726,7 @@ ${reportName} if (filters !== '') { text += ` ${filters}`; } - text += `${cards}で`; + text += `${cards} に対して`; return text; }, removeRule: ({cards}: {cards: string}) => `${cards} から支出ルールを削除しました`, @@ -7737,19 +7734,19 @@ ${reportName} update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `${cards} の支出ルールを ${fromAction} から ${toAction} に変更しました`, appliedToAdditionalCards: ({count}: {count: number}) => `${count} 枚の追加カードに支出ルールを適用しました`, - phraseVerb: {added: '追加しました', removed: '削除済み', changed: '変更しました', set: '設定', applied: '適用済み'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} な加盟店「${value}」` : `加盟店「${value}」`), + phraseVerb: {added: '追加しました', removed: '削除しました', changed: '変更しました', set: '設定', applied: '適用済み'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective}な加盟店「${value}」` : `加盟店「${value}」`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `${adjective} のマーチャントを「${oldValue}」から「${newValue}」に変更しました` : `加盟店を「${oldValue}」から「${newValue}」に変更しました`, - bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective}支出カテゴリ「${value}」` : `支出カテゴリ「${value}」`), + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective}な支出カテゴリ「${value}」` : `支出カテゴリ「${value}」`), bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective}の支出カテゴリを「${oldValue}」から「${newValue}」に変更しました` : `支出カテゴリを「${oldValue}」から「${newValue}」に変更しました`, + adjective !== '' ? `${adjective}支出カテゴリを「${oldValue}」から「${newValue}」に変更しました` : `支出カテゴリを「${oldValue}」から「${newValue}」に変更しました`, bodyMaxAmount: '最大金額', bodyMaxAmountSet: ({value}: {value: string}) => `最大金額を${value}に設定`, - bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `上限金額を${oldValue}から${newValue}に変更しました`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `追加のカード ${count} 枚に支出ルールを適用します`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `最大金額を${oldValue}から${newValue}に変更しました`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `${count} 枚の追加カードに支出ルールを適用します`, bodyRemovedFromCards: ({cards}: {cards: string}) => `${cards} からの支出ルール`, - composeOnCards: ({content, cards}: {content: string; cards: string}) => `${cards} の ${content}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => `${cards} 上の ${content}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `${cards} からの ${content}`, }, }, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index e2d4a3421c96..a7a838c62603 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7794,22 +7794,22 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, text += `op ${cards}`; return text; }, - removeRule: ({cards}: {cards: string}) => `besteedregel verwijderd van ${cards}`, + removeRule: ({cards}: {cards: string}) => `uitgavenregel verwijderd van ${cards}`, restrictionVerb: {block: 'blokkeren', allow: 'alleen toestaan'}, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => - `heeft de bestedingsregel gewijzigd van ${fromAction} naar ${toAction} op ${cards}`, - appliedToAdditionalCards: ({count}: {count: number}) => `bestedingsregel toegepast op ${count} extra kaarten`, + `heeft bestedingsregel op ${cards} gewijzigd van ${fromAction} naar ${toAction}`, + appliedToAdditionalCards: ({count}: {count: number}) => `uitgavenregel toegepast op ${count} extra kaarten`, phraseVerb: {added: 'toegevoegd', removed: 'verwijderd', changed: 'gewijzigd', set: 'instellen', applied: 'toegepast'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} handelaar '${value}'` : `handelaar '${value}'`), + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} handelaar ‘${value}’` : `handelaar '${value}'`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `${adjective} handelaar gewijzigd van '${oldValue}' naar '${newValue}'` : `handelaar van '${oldValue}' naar '${newValue}'`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => adjective !== '' ? `${adjective} uitgavencategorie '${value}'` : `uitgavencategorie ‘${value}’`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} uitgavencategorie van '${oldValue}' naar '${newValue}'` : `bestedingscategorie van '${oldValue}' naar '${newValue}'`, - bodyMaxAmount: 'maximumbedrag', - bodyMaxAmountSet: ({value}: {value: string}) => `max. bedrag tot ${value}`, + adjective !== '' ? `${adjective} uitgavencategorie van '${oldValue}' naar '${newValue}'` : `uitgavencategorie van '${oldValue}' naar '${newValue}'`, + bodyMaxAmount: 'max. bedrag', + bodyMaxAmountSet: ({value}: {value: string}) => `maximaal bedrag tot ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `maximumbedrag van ${oldValue} naar ${newValue}`, bodyAppliedToAdditionalCards: ({count}: {count: number}) => `bestedingsregel naar ${count} extra kaarten`, bodyRemovedFromCards: ({cards}: {cards: string}) => `bestedingsregel van ${cards}`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 5dbfe04d45d8..42a2fd344db8 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7790,17 +7790,16 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, removeRule: ({cards}: {cards: string}) => `usunięto regułę wydatków z ${cards}`, restrictionVerb: {block: 'zablokuj', allow: 'zezwól tylko'}, update: { - modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => - `zmienił(-a) regułę wydatków z ${fromAction} na ${toAction} na ${cards}`, + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `zmieniono regułę wydatków z ${fromAction} na ${toAction} na ${cards}`, appliedToAdditionalCards: ({count}: {count: number}) => `zastosowano regułę wydatków do ${count} dodatkowych kart`, phraseVerb: {added: 'dodano', removed: 'usunięto', changed: 'zmieniono', set: 'ustaw', applied: 'zastosowano'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} sprzedawca '${value}'` : `sprzedawca „${value}”`), + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} sprzedawca „${value}”` : `sprzedawca „${value}”`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `${adjective} sprzedawcę z „${oldValue}” na „${newValue}”` : `sprzedawcę z „${oldValue}” na „${newValue}”`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => adjective !== '' ? `${adjective} kategoria wydatków „${value}”` : `kategoria wydatków „${value}”`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} kategorię wydatków z „${oldValue}” na „${newValue}”` : `kategoria wydatku z „${oldValue}” na „${newValue}”`, + adjective !== '' ? `${adjective} kategoria wydatków z „${oldValue}” na „${newValue}”` : `kategoria wydatków z „${oldValue}” na „${newValue}”`, bodyMaxAmount: 'maksymalna kwota', bodyMaxAmountSet: ({value}: {value: string}) => `maksymalna kwota do ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `maksymalną kwotę z ${oldValue} na ${newValue}`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 94337e982ff5..4ae366ef0d7c 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7762,7 +7762,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'ativado' : 'desativado'} o requisito de compras com cartão corporativo`, expensifyCardRule: { actionVerb: {block: 'bloqueado', allow: 'permitido'}, - amountOperator: {over: 'sobre', under: 'embaixo'}, + amountOperator: {over: 'sobre', under: 'abaixo'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `valores ${operator} ${amount}`, theCard: 'o cartão', multipleCards: ({count}: {count: number}) => `${count} cartões`, @@ -7787,23 +7787,23 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, return text; }, removeRule: ({cards}: {cards: string}) => `removeu a regra de gasto de ${cards}`, - restrictionVerb: {block: 'bloquear', allow: 'permitir somente'}, + restrictionVerb: {block: 'bloquear', allow: 'permitir apenas'}, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `alterou a regra de gasto de ${fromAction} para ${toAction} em ${cards}`, appliedToAdditionalCards: ({count}: {count: number}) => `regra de gasto aplicada a mais ${count} cartões`, phraseVerb: {added: 'adicionado', removed: 'removido', changed: 'alterado', set: 'definir', applied: 'aplicado'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `Comerciante ${adjective} '${value}'` : `estabelecimento '${value}'`), + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} comerciante '${value}'` : `estabelecimento '${value}'`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} comerciante de '${oldValue}' para '${newValue}'` : `estabelecimento de '${oldValue}' para '${newValue}'`, + adjective !== '' ? `${adjective} comerciante de '${oldValue}' para '${newValue}'` : `estabelecimento comercial de '${oldValue}' para '${newValue}'`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => - adjective !== '' ? `categoria de gasto ${adjective} '${value}'` : `categoria de gasto '${value}'`, + adjective !== '' ? `categoria de despesa ${adjective} '${value}'` : `categoria de gasto '${value}'`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `categoria de gasto ${adjective} de '${oldValue}' para '${newValue}'` : `categoria de gasto de '${oldValue}' para '${newValue}'`, + adjective !== '' ? `categoria de gasto ${adjective} de '${oldValue}' para '${newValue}'` : `categoria de gastos de '${oldValue}' para '${newValue}'`, bodyMaxAmount: 'valor máximo', - bodyMaxAmountSet: ({value}: {value: string}) => `valor máximo para ${value}`, + bodyMaxAmountSet: ({value}: {value: string}) => `valor máximo até ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `valor máximo de ${oldValue} para ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regra de gasto para mais ${count} cartões adicionais`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regra de gasto para mais ${count} cartões`, bodyRemovedFromCards: ({cards}: {cards: string}) => `regra de gasto de ${cards}`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} em ${cards}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} de ${cards}`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 89eceab18c67..267fae533a72 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7563,8 +7563,8 @@ ${reportName} addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `已添加 ${fieldType} 报告字段“${fieldName}”${defaultValue ? ` 默认值为“${defaultValue}”` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? '已启用' : '已禁用'} 公司商务卡消费要求`, expensifyCardRule: { - actionVerb: {block: '已阻止', allow: '已允许'}, - amountOperator: {over: '超过', under: '在……之下'}, + actionVerb: {block: '已阻止', allow: '允许'}, + amountOperator: {over: '结束', under: '在…之下'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `金额 ${operator} ${amount}`, theCard: '该卡', multipleCards: ({count}: {count: number}) => `${count} 张卡片`, @@ -7578,35 +7578,35 @@ ${reportName} if (items.length === 2) { return `${items.at(0)} 和 ${items.at(1)}`; } - return `${items.slice(0, -1).join(', ')},和 ${items.at(-1)}`; + return `${items.slice(0, -1).join(', ')},以及 ${items.at(-1)}`; }, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { text += ` ${filters}`; } - text += `于 ${cards}`; + text += `在 ${cards} 上`; return text; }, - removeRule: ({cards}: {cards: string}) => `已从 ${cards} 中移除消费规则`, + removeRule: ({cards}: {cards: string}) => `已从 ${cards} 移除消费规则`, restrictionVerb: {block: '屏蔽', allow: '仅允许'}, update: { - modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `已将 ${cards} 的消费规则从 ${fromAction} 更改为 ${toAction}`, + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `已将 ${cards} 的支出规则从 ${fromAction} 更改为 ${toAction}`, appliedToAdditionalCards: ({count}: {count: number}) => `已将消费规则应用到另外 ${count} 张卡片`, phraseVerb: {added: '已添加', removed: '已移除', changed: '已更改', set: '设置', applied: '已应用'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} 商户“${value}”` : `商家“${value}”`), + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} 商户“${value}”` : `商户“${value}”`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `将商家${adjective}从“${oldValue}”更改为“${newValue}”` : `商户从“${oldValue}”变为“${newValue}”`, + adjective !== '' ? `将商家${adjective}从“${oldValue}”更改为“${newValue}”` : `商户从“${oldValue}”变更为“${newValue}”`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} 支出类别“${value}”` : `支出类别“${value}”`), bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `将${adjective}支出类别从“${oldValue}”更改为“${newValue}”` : `将支出类别从“${oldValue}”更改为“${newValue}”`, - bodyMaxAmount: '最大金额', - bodyMaxAmountSet: ({value}: {value: string}) => `最高金额为 ${value}`, - bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `最大金额从 ${oldValue} 变为 ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `为另外 ${count} 张卡设置消费规则`, - bodyRemovedFromCards: ({cards}: {cards: string}) => `来自 ${cards} 的消费规则`, + adjective !== '' ? `将${adjective}支出类别从“${oldValue}”更改为“${newValue}”` : `将消费类别从“${oldValue}”更改为“${newValue}”`, + bodyMaxAmount: '最高金额', + bodyMaxAmountSet: ({value}: {value: string}) => `最大金额至 ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `最高金额从 ${oldValue} 改为 ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `对另外 ${count} 张卡设置消费规则`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `来自 ${cards} 的支出规则`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${cards} 上的 ${content}`, - composeFromCards: ({content, cards}: {content: string; cards: string}) => `来自${cards}的${content}`, + composeFromCards: ({content, cards}: {content: string; cards: string}) => `来自 ${cards} 的 ${content}`, }, }, }, From ce4e63887e3f1b3c50d93f634b9343cd0012f04f Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 26 May 2026 12:53:49 -0600 Subject: [PATCH 31/43] rm joinFilters --- src/languages/de.ts | 12 ------------ src/languages/en.ts | 12 ------------ src/languages/es.ts | 12 ------------ src/languages/fr.ts | 12 ------------ src/languages/it.ts | 12 ------------ src/languages/ja.ts | 12 ------------ src/languages/nl.ts | 12 ------------ src/languages/pl.ts | 12 ------------ src/languages/pt-BR.ts | 12 ------------ src/languages/zh-hans.ts | 12 ------------ src/libs/IntlPolyfill/polyfillListFormat.ts | 10 +++++++++- src/libs/SpendRuleChangeLogUtils.ts | 11 ++++++----- 12 files changed, 15 insertions(+), 126 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 91cec363be59..9652e8b6bd75 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7813,18 +7813,6 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc amountFilter: ({operator, amount}: {operator: string; amount: string}) => `Beträge ${operator} ${amount}`, theCard: 'die Karte', multipleCards: ({count}: {count: number}) => `${count} Karten`, - joinFilters: ({items}: {items: string[]}) => { - if (items.length === 0) { - return ''; - } - if (items.length === 1) { - return items.at(0) ?? ''; - } - if (items.length === 2) { - return `${items.at(0)} und ${items.at(1)}`; - } - return `${items.slice(0, -1).join(', ')} und ${items.at(-1)}`; - }, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { diff --git a/src/languages/en.ts b/src/languages/en.ts index 37010593341b..ee900b7a4c69 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7524,18 +7524,6 @@ const translations = { amountFilter: ({operator, amount}: {operator: string; amount: string}) => `amounts ${operator} ${amount}`, theCard: 'the card', multipleCards: ({count}: {count: number}) => `${count} cards`, - joinFilters: ({items}: {items: string[]}) => { - if (items.length === 0) { - return ''; - } - if (items.length === 1) { - return items.at(0) ?? ''; - } - if (items.length === 2) { - return `${items.at(0)} and ${items.at(1)}`; - } - return `${items.slice(0, -1).join(', ')}, and ${items.at(-1)}`; - }, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { diff --git a/src/languages/es.ts b/src/languages/es.ts index a5a3595cec08..6979e28fb9c2 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7303,18 +7303,6 @@ ${amount} para ${merchant} - ${date}`, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `importes ${operator} ${amount}`, theCard: 'la tarjeta', multipleCards: ({count}: {count: number}) => `${count} tarjetas`, - joinFilters: ({items}: {items: string[]}) => { - if (items.length === 0) { - return ''; - } - if (items.length === 1) { - return items.at(0) ?? ''; - } - if (items.length === 2) { - return `${items.at(0)} y ${items.at(1)}`; - } - return `${items.slice(0, -1).join(', ')} y ${items.at(-1)}`; - }, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 8404e2156a55..56ff2cba3352 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7843,18 +7843,6 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e amountFilter: ({operator, amount}: {operator: string; amount: string}) => `montants ${operator} ${amount}`, theCard: 'la carte', multipleCards: ({count}: {count: number}) => `${count} cartes`, - joinFilters: ({items}: {items: string[]}) => { - if (items.length === 0) { - return ''; - } - if (items.length === 1) { - return items.at(0) ?? ''; - } - if (items.length === 2) { - return `${items.at(0)} et ${items.at(1)}`; - } - return `${items.slice(0, -1).join(', ')} et ${items.at(-1)}`; - }, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { diff --git a/src/languages/it.ts b/src/languages/it.ts index 1a4042be6853..fd42de52dfa9 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7803,18 +7803,6 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `importi ${operator} ${amount}`, theCard: 'la carta', multipleCards: ({count}: {count: number}) => `${count} carte`, - joinFilters: ({items}: {items: string[]}) => { - if (items.length === 0) { - return ''; - } - if (items.length === 1) { - return items.at(0) ?? ''; - } - if (items.length === 2) { - return `${items.at(0)} e ${items.at(1)}`; - } - return `${items.slice(0, -1).join(', ')} e ${items.at(-1)}`; - }, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index d4bb4aa73647..541856a832e3 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7709,18 +7709,6 @@ ${reportName} amountFilter: ({operator, amount}: {operator: string; amount: string}) => `金額 ${operator} ${amount}`, theCard: 'カード', multipleCards: ({count}: {count: number}) => `${count} 枚のカード`, - joinFilters: ({items}: {items: string[]}) => { - if (items.length === 0) { - return ''; - } - if (items.length === 1) { - return items.at(0) ?? ''; - } - if (items.length === 2) { - return `${items.at(0)} と ${items.at(1)}`; - } - return `${items.slice(0, -1).join(', ')}、および ${items.at(-1)}`; - }, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index a7a838c62603..58cc1376cee5 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7774,18 +7774,6 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `bedragen ${operator} ${amount}`, theCard: 'de kaart', multipleCards: ({count}: {count: number}) => `${count} kaarten`, - joinFilters: ({items}: {items: string[]}) => { - if (items.length === 0) { - return ''; - } - if (items.length === 1) { - return items.at(0) ?? ''; - } - if (items.length === 2) { - return `${items.at(0)} en ${items.at(1)}`; - } - return `${items.slice(0, -1).join(', ')}, en ${items.at(-1)}`; - }, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 42a2fd344db8..ed9de6e9b778 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7767,18 +7767,6 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `kwoty ${operator} ${amount}`, theCard: 'karta', multipleCards: ({count}: {count: number}) => `${count} karty`, - joinFilters: ({items}: {items: string[]}) => { - if (items.length === 0) { - return ''; - } - if (items.length === 1) { - return items.at(0) ?? ''; - } - if (items.length === 2) { - return `${items.at(0)} i ${items.at(1)}`; - } - return `${items.slice(0, -1).join(', ')} i ${items.at(-1)}`; - }, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 4ae366ef0d7c..7de6a6b7edad 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7766,18 +7766,6 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `valores ${operator} ${amount}`, theCard: 'o cartão', multipleCards: ({count}: {count: number}) => `${count} cartões`, - joinFilters: ({items}: {items: string[]}) => { - if (items.length === 0) { - return ''; - } - if (items.length === 1) { - return items.at(0) ?? ''; - } - if (items.length === 2) { - return `${items.at(0)} e ${items.at(1)}`; - } - return `${items.slice(0, -1).join(', ')} e ${items.at(-1)}`; - }, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 267fae533a72..23de1c72c359 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7568,18 +7568,6 @@ ${reportName} amountFilter: ({operator, amount}: {operator: string; amount: string}) => `金额 ${operator} ${amount}`, theCard: '该卡', multipleCards: ({count}: {count: number}) => `${count} 张卡片`, - joinFilters: ({items}: {items: string[]}) => { - if (items.length === 0) { - return ''; - } - if (items.length === 1) { - return items.at(0) ?? ''; - } - if (items.length === 2) { - return `${items.at(0)} 和 ${items.at(1)}`; - } - return `${items.slice(0, -1).join(', ')},以及 ${items.at(-1)}`; - }, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { diff --git a/src/libs/IntlPolyfill/polyfillListFormat.ts b/src/libs/IntlPolyfill/polyfillListFormat.ts index 21bb26cadc4b..cc53ac404aca 100644 --- a/src/libs/IntlPolyfill/polyfillListFormat.ts +++ b/src/libs/IntlPolyfill/polyfillListFormat.ts @@ -5,7 +5,15 @@ export default function () { require('@formatjs/intl-listformat/polyfill-force'); - // Load en & es Locale data + // Load locale data for all supported locales. pt covers pt-BR; zh covers zh-hans. require('@formatjs/intl-listformat/locale-data/en'); require('@formatjs/intl-listformat/locale-data/es'); + require('@formatjs/intl-listformat/locale-data/de'); + require('@formatjs/intl-listformat/locale-data/fr'); + require('@formatjs/intl-listformat/locale-data/it'); + require('@formatjs/intl-listformat/locale-data/ja'); + require('@formatjs/intl-listformat/locale-data/nl'); + require('@formatjs/intl-listformat/locale-data/pl'); + require('@formatjs/intl-listformat/locale-data/pt'); + require('@formatjs/intl-listformat/locale-data/zh'); } diff --git a/src/libs/SpendRuleChangeLogUtils.ts b/src/libs/SpendRuleChangeLogUtils.ts index 6a9eac1141c3..2fcd1bf49be7 100644 --- a/src/libs/SpendRuleChangeLogUtils.ts +++ b/src/libs/SpendRuleChangeLogUtils.ts @@ -4,6 +4,7 @@ import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; import type ReportAction from '@src/types/onyx/ReportAction'; import {convertAmountToDisplayString} from './CurrencyUtils'; +import {formatList} from './Localize'; import Parser from './Parser'; import stripFollowupListFromHtml from './ReportActionFollowupUtils/stripFollowupListFromHtml'; import {getOriginalMessage, isActionOfType} from './ReportActionsUtils'; @@ -54,9 +55,9 @@ function getSpendRuleCardsSummary(translate: LocalizedTranslate, cards: Readonly return translate('workspaceActions.expensifyCardRule.multipleCards', {count: cards.length}); } -function getSpendRuleJoinFilters(translate: LocalizedTranslate, items: readonly string[]): string { +function getSpendRuleJoinFilters(items: readonly string[]): string { const filtered = items.filter((value) => typeof value === 'string' && value !== ''); - return translate('workspaceActions.expensifyCardRule.joinFilters', {items: filtered}); + return formatList(filtered); } function getSpendRuleCategoryDisplayName(translate: LocalizedTranslate, category: string): string { @@ -196,7 +197,7 @@ function joinSpendRulePhrases(translate: LocalizedTranslate, phrases: readonly S if (!allSameVerb) { const parts = phrases.map((phrase) => `${getSpendRulePhraseVerbWord(translate, phrase.verb)} ${phrase.bodyWithAdjective}`); - return getSpendRuleJoinFilters(translate, parts); + return getSpendRuleJoinFilters(parts); } const firstPhrase = phrases.at(0); @@ -213,7 +214,7 @@ function joinSpendRulePhrases(translate: LocalizedTranslate, phrases: readonly S const useOwnAdjective = phrase.adjective !== '' && phrase.adjective !== firstAdjective; parts.push(useOwnAdjective ? phrase.bodyWithAdjective : phrase.bodyWithoutAdjective); } - return getSpendRuleJoinFilters(translate, parts); + return getSpendRuleJoinFilters(parts); } function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { @@ -243,7 +244,7 @@ function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAct } const verb = getSpendRuleActionVerb(translate, action); - const filters = getSpendRuleJoinFilters(translate, items); + const filters = getSpendRuleJoinFilters(items); const cardsSummary = getSpendRuleCardsSummary(translate, cards); if (verb === '') { From 2c1df6877252da452f2b09e31c8a0a4887b83f2e Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 27 May 2026 10:00:36 -0600 Subject: [PATCH 32/43] fix spanish copy --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 2074584efda9..a0b3516eb340 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7316,7 +7316,7 @@ ${amount} para ${merchant} - ${date}`, if (filters !== '') { text += ` ${filters}`; } - text += `en ${cards}`; + text += ` en ${cards}`; return text; }, removeRule: ({cards}: {cards: string}) => `eliminó la regla de gasto de ${cards}`, From 72f1ddcd43cc33b0944e21d6f5d666bbc5e752db Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 27 May 2026 10:08:24 -0600 Subject: [PATCH 33/43] update plural rules --- src/languages/en.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index fcee7188ef9f..6f91b596a294 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7523,7 +7523,10 @@ const translations = { }, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `amounts ${operator} ${amount}`, theCard: 'the card', - multipleCards: ({count}: {count: number}) => `${count} cards`, + multipleCards: ({count}: {count: number}) => ({ + one: '1 card', + other: `${count} cards`, + }), addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { @@ -7539,7 +7542,10 @@ const translations = { }, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `changed spend rule from ${fromAction} to ${toAction} on ${cards}`, - appliedToAdditionalCards: ({count}: {count: number}) => `applied spend rule to ${count} additional cards`, + appliedToAdditionalCards: ({count}: {count: number}) => ({ + one: 'applied spend rule to 1 additional card', + other: `applied spend rule to ${count} additional cards`, + }), phraseVerb: { added: 'added', removed: 'removed', @@ -7556,7 +7562,10 @@ const translations = { bodyMaxAmount: 'max amount', bodyMaxAmountSet: ({value}: {value: string}) => `max amount to ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `max amount from ${oldValue} to ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `spend rule to ${count} additional cards`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => ({ + one: 'spend rule to 1 additional card', + other: `spend rule to ${count} additional cards`, + }), bodyRemovedFromCards: ({cards}: {cards: string}) => `spend rule from ${cards}`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} on ${cards}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} from ${cards}`, From 82d8bdd06fa0cf50c52aeb69d25c15865293a772 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 27 May 2026 10:15:09 -0600 Subject: [PATCH 34/43] apply translations --- src/languages/de.ts | 18 +++++++++--------- src/languages/es.ts | 14 +++++++------- src/languages/fr.ts | 12 ++++++------ src/languages/it.ts | 12 ++++++------ src/languages/ja.ts | 23 +++++++++++++---------- src/languages/nl.ts | 12 ++++++------ src/languages/pl.ts | 10 +++++----- src/languages/pt-BR.ts | 8 ++++---- src/languages/zh-hans.ts | 28 ++++++++++++++-------------- 9 files changed, 70 insertions(+), 67 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 3b10277a41c2..6eeb81387230 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7826,22 +7826,22 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc return text; }, removeRule: ({cards}: {cards: string}) => `Ausgaberegel von ${cards} entfernt`, - restrictionVerb: {block: 'Block', allow: 'nur erlauben'}, + restrictionVerb: {block: 'Block', allow: 'nur zulassen'}, update: { - modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `Ausgaberegel von ${fromAction} auf ${toAction} auf ${cards} geändert`, + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `Ausgaberegel von ${fromAction} zu ${toAction} auf ${cards} geändert`, appliedToAdditionalCards: ({count}: {count: number}) => `Ausgaberegel auf ${count} weitere Karten angewendet`, - phraseVerb: {added: 'hinzugefügt', removed: 'entfernt', changed: 'geändert', set: 'festlegen', applied: 'angewendet'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} Händler '${value}'` : `Händler: '${value}'`), + phraseVerb: {added: 'hinzugefügt', removed: 'entfernt', changed: 'geändert', set: 'festlegen', applied: 'Angewendet'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} Händler: „${value}“` : `Händler: „${value}“`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} Händler von „${oldValue}“ in „${newValue}“` : `Händler von „${oldValue}“ zu „${newValue}“`, + adjective !== '' ? `${adjective} Händler von „${oldValue}“ in „${newValue}“ geändert` : `Händler von „${oldValue}“ zu „${newValue}“`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => adjective !== '' ? `${adjective} Ausgabenkategorie „${value}“` : `Ausgabenkategorie „${value}“`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} Ausgabenkategorie von „${oldValue}“ in „${newValue}“` : `Ausgabenkategorie von „${oldValue}“ zu „${newValue}“`, - bodyMaxAmount: 'Maximalbetrag', - bodyMaxAmountSet: ({value}: {value: string}) => `Maximalbetrag von ${value}`, + adjective !== '' ? `Ausgabenkategorie (${adjective}) von „${oldValue}“ in „${newValue}“ geändert` : `Ausgabenkategorie von „${oldValue}“ zu „${newValue}“`, + bodyMaxAmount: 'Höchstbetrag', + bodyMaxAmountSet: ({value}: {value: string}) => `Maximalbetrag bis ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `Maximalbetrag von ${oldValue} auf ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `Ausgabenregel auf ${count} zusätzliche Karten`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `Ausgabenregel für ${count} zusätzliche Karten`, bodyRemovedFromCards: ({cards}: {cards: string}) => `Ausgaberegel von ${cards}`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} auf ${cards}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} von ${cards}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index a0b3516eb340..79cc225930c3 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7307,8 +7307,8 @@ ${amount} para ${merchant} - ${date}`, `cambió el día de cierre del período de estado de cuenta de la fuente de tarjetas "${feedName}"${newValue ? ` a "${newValue}"` : ''}${previousValue ? ` (previamente "${previousValue}")` : ''}`, expensifyCardRule: { actionVerb: {block: 'bloqueado', allow: 'permitido'}, - amountOperator: {over: 'encima', under: 'debajo'}, - amountFilter: ({operator, amount}: {operator: string; amount: string}) => `importes ${operator} ${amount}`, + amountOperator: {over: 'más de', under: 'debajo'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `cantidades ${operator} ${amount}`, theCard: 'la tarjeta', multipleCards: ({count}: {count: number}) => `${count} tarjetas`, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { @@ -7324,16 +7324,16 @@ ${amount} para ${merchant} - ${date}`, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `cambió la regla de gasto de ${fromAction} a ${toAction} en ${cards}`, appliedToAdditionalCards: ({count}: {count: number}) => `aplicó la regla de gasto a ${count} tarjetas adicionales`, - phraseVerb: {added: 'añadió', removed: 'eliminado', changed: 'cambió', set: 'establecer', applied: 'aplicado'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `Comercio ${adjective} «${value}»` : `comercio «${value}»`), + phraseVerb: {added: 'añadido', removed: 'eliminado', changed: 'cambió', set: 'establecer', applied: 'aplicado'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} comercio «${value}»` : `comercio «${value}»`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `comercio ${adjective} de '${oldValue}' a '${newValue}'` : `comercio de '${oldValue}' a '${newValue}'`, + adjective !== '' ? `${adjective} del comerciante de '${oldValue}' a '${newValue}'` : `comercio de '${oldValue}' a '${newValue}'`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => adjective !== '' ? `Categoría de gasto ${adjective} «${value}»` : `categoría de gasto «${value}»`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `Categoría de gasto ${adjective} de '${oldValue}' a '${newValue}'` : `categoría de gasto de «${oldValue}» a «${newValue}»`, + adjective !== '' ? `Categoría de gasto ${adjective} de '${oldValue}' a '${newValue}'` : `categoría de gasto de '${oldValue}' a '${newValue}'`, bodyMaxAmount: 'importe máximo', - bodyMaxAmountSet: ({value}: {value: string}) => `importe máximo de ${value}`, + bodyMaxAmountSet: ({value}: {value: string}) => `importe máximo hasta ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `importe máximo de ${oldValue} a ${newValue}`, bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regla de gasto para ${count} tarjetas adicionales`, bodyRemovedFromCards: ({cards}: {cards: string}) => `regla de gasto de ${cards}`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index e3364ebe3ab9..fea1fc048c1b 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7861,19 +7861,19 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `a modifié la règle de dépense de ${fromAction} à ${toAction} sur ${cards}`, appliedToAdditionalCards: ({count}: {count: number}) => `règle de dépense appliquée à ${count} cartes supplémentaires`, - phraseVerb: {added: 'ajouté', removed: 'Supprimé', changed: 'modifié', set: 'définir', applied: 'appliqué'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `Commerçant ${adjective} « ${value} »` : `commerçant « ${value} »`), + phraseVerb: {added: 'ajouté', removed: 'supprimé', changed: 'modifié', set: 'définir', applied: 'appliqué'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} commerçant « ${value} »` : `commerçant « ${value} »`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `Commerçant ${adjective} de « ${oldValue} » à « ${newValue} »` : `commerçant de « ${oldValue} » à « ${newValue} »`, + adjective !== '' ? `${adjective} marchand de « ${oldValue} » à « ${newValue} »` : `commerçant de « ${oldValue} » à « ${newValue} »`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => - adjective !== '' ? `Catégorie de dépenses ${adjective} « ${value} »` : `catégorie de dépense « ${value} »`, + adjective !== '' ? `Catégorie de dépense ${adjective} « ${value} »` : `catégorie de dépense « ${value} »`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `Catégorie de dépense ${adjective} de « ${oldValue} » à « ${newValue} »` : `catégorie de dépense de « ${oldValue} » à « ${newValue} »`, bodyMaxAmount: 'montant maximal', - bodyMaxAmountSet: ({value}: {value: string}) => `montant maximal à ${value}`, + bodyMaxAmountSet: ({value}: {value: string}) => `montant maximum à ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `montant maximal de ${oldValue} à ${newValue}`, bodyAppliedToAdditionalCards: ({count}: {count: number}) => `règle de dépense pour ${count} cartes supplémentaires`, - bodyRemovedFromCards: ({cards}: {cards: string}) => `règle de dépense à partir de ${cards}`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `règle de dépense provenant de ${cards}`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} sur ${cards}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} de ${cards}`, }, diff --git a/src/languages/it.ts b/src/languages/it.ts index 5361e26332f2..ebd2e3e27bef 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7802,7 +7802,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'abilitato' : 'disabilitato'} il requisito per gli acquisti con carta aziendale`, expensifyCardRule: { actionVerb: {block: 'bloccato', allow: 'consentito'}, - amountOperator: {over: 'oltre', under: 'sotto'}, + amountOperator: {over: 'terminato', under: 'sotto'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `importi ${operator} ${amount}`, theCard: 'la carta', multipleCards: ({count}: {count: number}) => `${count} carte`, @@ -7814,22 +7814,22 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, text += `su ${cards}`; return text; }, - removeRule: ({cards}: {cards: string}) => `ha rimosso la regola di spesa da ${cards}`, + removeRule: ({cards}: {cards: string}) => `regola di spesa rimossa da ${cards}`, restrictionVerb: {block: 'blocca', allow: 'consenti solo'}, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `ha modificato la regola di spesa da ${fromAction} a ${toAction} su ${cards}`, - appliedToAdditionalCards: ({count}: {count: number}) => `regola di spesa applicata a ${count} carte aggiuntive`, + appliedToAdditionalCards: ({count}: {count: number}) => `ha applicato la regola di spesa ad altre ${count} carte`, phraseVerb: {added: 'aggiunto', removed: 'rimosso', changed: 'modificato', set: 'imposta', applied: 'applicata'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `commerciante ${adjective} '${value}'` : `esercente '${value}'`), + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} esercente '${value}'` : `esercente '${value}'`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `esercente ${adjective} da '${oldValue}' a '${newValue}'` : `esercente da '${oldValue}' a '${newValue}'`, + adjective !== '' ? `commerciante ${adjective} da '${oldValue}' a '${newValue}'` : `esercente da '${oldValue}' a '${newValue}'`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => adjective !== '' ? `Categoria di spesa ${adjective} '${value}'` : `categoria di spesa '${value}'`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `categoria di spesa ${adjective} da '${oldValue}' a '${newValue}'` : `categoria di spesa da '${oldValue}' a '${newValue}'`, bodyMaxAmount: 'importo massimo', - bodyMaxAmountSet: ({value}: {value: string}) => `importo massimo fino a ${value}`, + bodyMaxAmountSet: ({value}: {value: string}) => `importo massimo a ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `importo massimo da ${oldValue} a ${newValue}`, bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regola di spesa per ${count} carte aggiuntive`, bodyRemovedFromCards: ({cards}: {cards: string}) => `regola di spesa da ${cards}`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 0e2807060dce..b9cd6e332475 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7705,9 +7705,12 @@ ${reportName} `${fieldType}レポートフィールド「${fieldName}」を追加しました${defaultValue ? ` デフォルト値「${defaultValue}」付き` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? '有効' : '無効'} の法人カード購入要件`, expensifyCardRule: { - actionVerb: {block: 'ブロック済み', allow: '許可されています'}, - amountOperator: {over: '終了', under: '以下のいずれかの意味に応じて使い分けますが、一般的なUI文脈では「以下」となることが多いです。'}, - amountFilter: ({operator, amount}: {operator: string; amount: string}) => `金額 ${operator} ${amount}`, + actionVerb: {block: 'ブロック済み', allow: '許可済み'}, + amountOperator: { + over: '以上', + under: '以下のいずれかの意味に応じてお使いください: \n- 位置・場所:「〜の下」→「under」=「〜の下」 \n- 条件・範囲:「〜のもとで/〜以下」→「under 18」=「18歳未満」', + }, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `金額が ${amount} を${operator}`, theCard: 'カード', multipleCards: ({count}: {count: number}) => `${count} 枚のカード`, addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { @@ -7715,25 +7718,25 @@ ${reportName} if (filters !== '') { text += ` ${filters}`; } - text += `${cards} に対して`; + text += `(${cards})で`; return text; }, removeRule: ({cards}: {cards: string}) => `${cards} から支出ルールを削除しました`, restrictionVerb: {block: 'ブロック', allow: 'のみ許可'}, update: { - modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `${cards} の支出ルールを ${fromAction} から ${toAction} に変更しました`, - appliedToAdditionalCards: ({count}: {count: number}) => `${count} 枚の追加カードに支出ルールを適用しました`, - phraseVerb: {added: '追加しました', removed: '削除しました', changed: '変更しました', set: '設定', applied: '適用済み'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective}な加盟店「${value}」` : `加盟店「${value}」`), + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `${cards} の利用ルールを ${fromAction} から ${toAction} に変更しました`, + appliedToAdditionalCards: ({count}: {count: number}) => `追加で ${count} 枚のカードに支出ルールを適用しました`, + phraseVerb: {added: '追加しました', removed: '削除済み', changed: '変更しました', set: '設定', applied: '適用済み'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective}なマーチャント「${value}」` : `加盟店「${value}」`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} のマーチャントを「${oldValue}」から「${newValue}」に変更しました` : `加盟店を「${oldValue}」から「${newValue}」に変更しました`, + adjective !== '' ? `${oldValue} から ${newValue} へ${adjective}加盟店を変更しました` : `加盟店を「${oldValue}」から「${newValue}」に変更しました`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective}な支出カテゴリ「${value}」` : `支出カテゴリ「${value}」`), bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `${adjective}支出カテゴリを「${oldValue}」から「${newValue}」に変更しました` : `支出カテゴリを「${oldValue}」から「${newValue}」に変更しました`, bodyMaxAmount: '最大金額', bodyMaxAmountSet: ({value}: {value: string}) => `最大金額を${value}に設定`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `最大金額を${oldValue}から${newValue}に変更しました`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `${count} 枚の追加カードに支出ルールを適用します`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `${count}枚の追加カードに支出ルールを適用します`, bodyRemovedFromCards: ({cards}: {cards: string}) => `${cards} からの支出ルール`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${cards} 上の ${content}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `${cards} からの ${content}`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 06d800029a92..672d734e5c09 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7784,22 +7784,22 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, text += `op ${cards}`; return text; }, - removeRule: ({cards}: {cards: string}) => `uitgavenregel verwijderd van ${cards}`, + removeRule: ({cards}: {cards: string}) => `besteedregel verwijderd van ${cards}`, restrictionVerb: {block: 'blokkeren', allow: 'alleen toestaan'}, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => - `heeft bestedingsregel op ${cards} gewijzigd van ${fromAction} naar ${toAction}`, + `heeft bestedingsregel gewijzigd van ${fromAction} naar ${toAction} op ${cards}`, appliedToAdditionalCards: ({count}: {count: number}) => `uitgavenregel toegepast op ${count} extra kaarten`, phraseVerb: {added: 'toegevoegd', removed: 'verwijderd', changed: 'gewijzigd', set: 'instellen', applied: 'toegepast'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} handelaar ‘${value}’` : `handelaar '${value}'`), + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} handelaar '${value}'` : `handelaar '${value}'`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} handelaar gewijzigd van '${oldValue}' naar '${newValue}'` : `handelaar van '${oldValue}' naar '${newValue}'`, + adjective !== '' ? `${adjective} verkoper gewijzigd van '${oldValue}' naar '${newValue}'` : `handelaar van '${oldValue}' naar '${newValue}'`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => adjective !== '' ? `${adjective} uitgavencategorie '${value}'` : `uitgavencategorie ‘${value}’`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `${adjective} uitgavencategorie van '${oldValue}' naar '${newValue}'` : `uitgavencategorie van '${oldValue}' naar '${newValue}'`, - bodyMaxAmount: 'max. bedrag', - bodyMaxAmountSet: ({value}: {value: string}) => `maximaal bedrag tot ${value}`, + bodyMaxAmount: 'maximum bedrag', + bodyMaxAmountSet: ({value}: {value: string}) => `maximumbedrag tot ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `maximumbedrag van ${oldValue} naar ${newValue}`, bodyAppliedToAdditionalCards: ({count}: {count: number}) => `bestedingsregel naar ${count} extra kaarten`, bodyRemovedFromCards: ({cards}: {cards: string}) => `bestedingsregel van ${cards}`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 152fc6878396..9ed40d91466b 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7775,23 +7775,23 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, return text; }, removeRule: ({cards}: {cards: string}) => `usunięto regułę wydatków z ${cards}`, - restrictionVerb: {block: 'zablokuj', allow: 'zezwól tylko'}, + restrictionVerb: {block: 'zablokuj', allow: 'zezwalaj tylko'}, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `zmieniono regułę wydatków z ${fromAction} na ${toAction} na ${cards}`, appliedToAdditionalCards: ({count}: {count: number}) => `zastosowano regułę wydatków do ${count} dodatkowych kart`, phraseVerb: {added: 'dodano', removed: 'usunięto', changed: 'zmieniono', set: 'ustaw', applied: 'zastosowano'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} sprzedawca „${value}”` : `sprzedawca „${value}”`), + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} sprzedawca '${value}'` : `sprzedawca „${value}”`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} sprzedawcę z „${oldValue}” na „${newValue}”` : `sprzedawcę z „${oldValue}” na „${newValue}”`, + adjective !== '' ? `zmieniono ${adjective} sprzedawcę z „${oldValue}” na „${newValue}”` : `sprzedawcę z „${oldValue}” na „${newValue}”`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => adjective !== '' ? `${adjective} kategoria wydatków „${value}”` : `kategoria wydatków „${value}”`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `${adjective} kategoria wydatków z „${oldValue}” na „${newValue}”` : `kategoria wydatków z „${oldValue}” na „${newValue}”`, + adjective !== '' ? `${adjective} kategorię wydatków z „${oldValue}” na „${newValue}”` : `kategorię wydatku z „${oldValue}” na „${newValue}”`, bodyMaxAmount: 'maksymalna kwota', bodyMaxAmountSet: ({value}: {value: string}) => `maksymalna kwota do ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `maksymalną kwotę z ${oldValue} na ${newValue}`, bodyAppliedToAdditionalCards: ({count}: {count: number}) => `zasada wydatków dla ${count} dodatkowych kart`, - bodyRemovedFromCards: ({cards}: {cards: string}) => `reguła wydatków z ${cards}`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `zasada wydatków z ${cards}`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} na ${cards}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} z ${cards}`, }, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 73167564c479..ee3a84e7c281 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7764,7 +7764,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'ativado' : 'desativado'} o requisito de compras com cartão corporativo`, expensifyCardRule: { actionVerb: {block: 'bloqueado', allow: 'permitido'}, - amountOperator: {over: 'sobre', under: 'abaixo'}, + amountOperator: {over: 'acima', under: 'abaixo'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `valores ${operator} ${amount}`, theCard: 'o cartão', multipleCards: ({count}: {count: number}) => `${count} cartões`, @@ -7783,13 +7783,13 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, `alterou a regra de gasto de ${fromAction} para ${toAction} em ${cards}`, appliedToAdditionalCards: ({count}: {count: number}) => `regra de gasto aplicada a mais ${count} cartões`, phraseVerb: {added: 'adicionado', removed: 'removido', changed: 'alterado', set: 'definir', applied: 'aplicado'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} comerciante '${value}'` : `estabelecimento '${value}'`), + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `comerciante ${adjective} '${value}'` : `estabelecimento comercial '${value}'`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => adjective !== '' ? `${adjective} comerciante de '${oldValue}' para '${newValue}'` : `estabelecimento comercial de '${oldValue}' para '${newValue}'`, bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => - adjective !== '' ? `categoria de despesa ${adjective} '${value}'` : `categoria de gasto '${value}'`, + adjective !== '' ? `Categoria de gasto ${adjective} '${value}'` : `categoria de gastos '${value}'`, bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `categoria de gasto ${adjective} de '${oldValue}' para '${newValue}'` : `categoria de gastos de '${oldValue}' para '${newValue}'`, + adjective !== '' ? `Categoria de gasto ${adjective} de '${oldValue}' para '${newValue}'` : `categoria de gasto de '${oldValue}' para '${newValue}'`, bodyMaxAmount: 'valor máximo', bodyMaxAmountSet: ({value}: {value: string}) => `valor máximo até ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `valor máximo de ${oldValue} para ${newValue}`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 8c3f53dae5e6..9190ed4175d5 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7561,7 +7561,7 @@ ${reportName} updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? '已启用' : '已禁用'} 公司商务卡消费要求`, expensifyCardRule: { actionVerb: {block: '已阻止', allow: '允许'}, - amountOperator: {over: '结束', under: '在…之下'}, + amountOperator: {over: '结束', under: '在……之下'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `金额 ${operator} ${amount}`, theCard: '该卡', multipleCards: ({count}: {count: number}) => `${count} 张卡片`, @@ -7570,26 +7570,26 @@ ${reportName} if (filters !== '') { text += ` ${filters}`; } - text += `在 ${cards} 上`; + text += `在 ${cards}`; return text; }, - removeRule: ({cards}: {cards: string}) => `已从 ${cards} 移除消费规则`, - restrictionVerb: {block: '屏蔽', allow: '仅允许'}, + removeRule: ({cards}: {cards: string}) => `已从 ${cards} 中移除消费规则`, + restrictionVerb: {block: '阻止', allow: '仅允许'}, update: { - modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `已将 ${cards} 的支出规则从 ${fromAction} 更改为 ${toAction}`, - appliedToAdditionalCards: ({count}: {count: number}) => `已将消费规则应用到另外 ${count} 张卡片`, + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `已将 ${cards} 的消费规则从 ${fromAction} 更改为 ${toAction}`, + appliedToAdditionalCards: ({count}: {count: number}) => `已将支出规则应用于另外 ${count} 张卡片`, phraseVerb: {added: '已添加', removed: '已移除', changed: '已更改', set: '设置', applied: '已应用'}, - bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} 商户“${value}”` : `商户“${value}”`), + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} 商家“${value}”` : `商户“${value}”`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `将商家${adjective}从“${oldValue}”更改为“${newValue}”` : `商户从“${oldValue}”变更为“${newValue}”`, - bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} 支出类别“${value}”` : `支出类别“${value}”`), + adjective !== '' ? `将商家 ${adjective} 从“${oldValue}”更改为“${newValue}”` : `商户从“${oldValue}”变更为“${newValue}”`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective}支出类别“${value}”` : `支出类别「${value}」`), bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => - adjective !== '' ? `将${adjective}支出类别从“${oldValue}”更改为“${newValue}”` : `将消费类别从“${oldValue}”更改为“${newValue}”`, - bodyMaxAmount: '最高金额', + adjective !== '' ? `将${adjective}支出类别从“${oldValue}”修改为“${newValue}”` : `支出类别从“${oldValue}”更改为“${newValue}”`, + bodyMaxAmount: '最大金额', bodyMaxAmountSet: ({value}: {value: string}) => `最大金额至 ${value}`, - bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `最高金额从 ${oldValue} 改为 ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `对另外 ${count} 张卡设置消费规则`, - bodyRemovedFromCards: ({cards}: {cards: string}) => `来自 ${cards} 的支出规则`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `最大金额从 ${oldValue} 变为 ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `将消费规则应用到另外 ${count} 张卡片`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `来自 ${cards} 的消费规则`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${cards} 上的 ${content}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `来自 ${cards} 的 ${content}`, }, From acd0acf948901a7bf70f9324a259e69666c1930e Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 27 May 2026 10:29:02 -0600 Subject: [PATCH 35/43] update tests --- src/libs/SpendRuleChangeLogUtils.ts | 2 +- tests/unit/SpendRuleChangeLogUtilsTest.ts | 28 +++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/libs/SpendRuleChangeLogUtils.ts b/src/libs/SpendRuleChangeLogUtils.ts index 2fcd1bf49be7..e22232daf193 100644 --- a/src/libs/SpendRuleChangeLogUtils.ts +++ b/src/libs/SpendRuleChangeLogUtils.ts @@ -50,7 +50,7 @@ function getSpendRuleCardsSummary(translate: LocalizedTranslate, cards: Readonly } if (cards.length === 1) { const displayName = cards.at(0)?.displayName ?? ''; - return displayName !== '' ? displayName : translate('workspaceActions.expensifyCardRule.theCard'); + return displayName !== '' ? `'${displayName}'` : translate('workspaceActions.expensifyCardRule.theCard'); } return translate('workspaceActions.expensifyCardRule.multipleCards', {count: cards.length}); } diff --git a/tests/unit/SpendRuleChangeLogUtilsTest.ts b/tests/unit/SpendRuleChangeLogUtilsTest.ts index 772c191ef696..8ca5033fe55c 100644 --- a/tests/unit/SpendRuleChangeLogUtilsTest.ts +++ b/tests/unit/SpendRuleChangeLogUtilsTest.ts @@ -45,7 +45,7 @@ describe('SpendRuleChangeLogUtils', () => { cards: [{cardID: 1, displayName: 'My Visa'}], }, } as ReportAction; - expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe('allowed on My Visa'); + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("allowed on 'My Visa'"); }); it('returns block message with merchant filter', () => { @@ -59,7 +59,7 @@ describe('SpendRuleChangeLogUtils', () => { cards: [{cardID: 1, displayName: 'My Visa'}], }, } as ReportAction; - expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("blocked 'Starbucks' on My Visa"); + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("blocked 'Starbucks' on 'My Visa'"); }); it('returns allow message with category filter', () => { @@ -73,7 +73,7 @@ describe('SpendRuleChangeLogUtils', () => { cards: [{cardID: 1, displayName: 'My Visa'}], }, } as ReportAction; - expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("allowed 'Dining' on My Visa"); + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("allowed 'Dining' on 'My Visa'"); }); it('returns block message with amount-over filter', () => { @@ -88,7 +88,7 @@ describe('SpendRuleChangeLogUtils', () => { cards: [{cardID: 1, displayName: 'My Visa'}], }, } as ReportAction; - expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe('blocked amounts over $100.00 on My Visa'); + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("blocked amounts over $100.00 on 'My Visa'"); }); it('returns allow message with merchant and amount-under filter joined', () => { @@ -104,7 +104,7 @@ describe('SpendRuleChangeLogUtils', () => { cards: [{cardID: 1, displayName: 'My Visa'}], }, } as ReportAction; - expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("allowed 'Amazon' and amounts under $50.00 on My Visa"); + expect(getAddExpensifyCardRuleMessage(translateLocal, action)).toBe("allowed 'Amazon' and amounts under $50.00 on 'My Visa'"); }); it('returns message with multiple-cards summary', () => { @@ -147,7 +147,7 @@ describe('SpendRuleChangeLogUtils', () => { cards: [{cardID: 1, displayName: 'My Visa'}], }, } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('changed spend rule from only allow to block on My Visa'); + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe("changed spend rule from only allow to block on 'My Visa'"); }); it('returns applied-to-additional-cards message when only new cards were added', () => { @@ -164,7 +164,7 @@ describe('SpendRuleChangeLogUtils', () => { ], }, } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('applied spend rule to 1 additional cards'); + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('applied spend rule to 1 additional card'); }); it('returns remove-rule message when only cards were removed', () => { @@ -181,7 +181,7 @@ describe('SpendRuleChangeLogUtils', () => { cards: [{cardID: 1, displayName: 'Card A'}], }, } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('removed spend rule from Card B'); + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe("removed spend rule from 'Card B'"); }); it('returns added-merchant message with adjective for allow action', () => { @@ -197,7 +197,7 @@ describe('SpendRuleChangeLogUtils', () => { cards: [{cardID: 1, displayName: 'My Visa'}], }, } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe("added allowed merchant 'Starbucks' on My Visa"); + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe("added allowed merchant 'Starbucks' on 'My Visa'"); }); it('returns changed-category message when single category was swapped', () => { @@ -213,7 +213,7 @@ describe('SpendRuleChangeLogUtils', () => { cards: [{cardID: 1, displayName: 'My Visa'}], }, } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe("changed blocked spend category from 'Airlines' to 'Dining' on My Visa"); + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe("changed blocked spend category from 'Airlines' to 'Dining' on 'My Visa'"); }); it('returns set-max-amount message when amount is newly added', () => { @@ -230,7 +230,7 @@ describe('SpendRuleChangeLogUtils', () => { cards: [{cardID: 1, displayName: 'My Visa'}], }, } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('set max amount to $100.00 on My Visa'); + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe("set max amount to $100.00 on 'My Visa'"); }); it('returns changed-max-amount message when amount value changes', () => { @@ -247,7 +247,7 @@ describe('SpendRuleChangeLogUtils', () => { cards: [{cardID: 1, displayName: 'My Visa'}], }, } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('changed max amount from $50.00 to $100.00 on My Visa'); + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe("changed max amount from $50.00 to $100.00 on 'My Visa'"); }); it('returns removed-max-amount message when amount is cleared', () => { @@ -264,7 +264,7 @@ describe('SpendRuleChangeLogUtils', () => { cards: [{cardID: 1, displayName: 'My Visa'}], }, } as ReportAction; - expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe('removed max amount from My Visa'); + expect(getUpdateExpensifyCardRuleMessage(translateLocal, action)).toBe("removed max amount from 'My Visa'"); }); }); @@ -288,7 +288,7 @@ describe('SpendRuleChangeLogUtils', () => { cards: [{cardID: 1, displayName: 'My Visa'}], }, } as ReportAction; - expect(getRemoveExpensifyCardRuleMessage(translateLocal, action)).toBe('removed spend rule from My Visa'); + expect(getRemoveExpensifyCardRuleMessage(translateLocal, action)).toBe("removed spend rule from 'My Visa'"); }); it('returns message with multiple-cards summary', () => { From eaecdea4432e220f8e7f553305f288012bbc28b4 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 27 May 2026 12:11:29 -0600 Subject: [PATCH 36/43] fix locale copy --- src/languages/de.ts | 2 +- src/languages/fr.ts | 2 +- src/languages/it.ts | 2 +- src/languages/nl.ts | 2 +- src/languages/pl.ts | 2 +- src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 6eeb81387230..c79e803fd42f 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7822,7 +7822,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc if (filters !== '') { text += ` ${filters}`; } - text += `auf ${cards}`; + text += ` auf ${cards}`; return text; }, removeRule: ({cards}: {cards: string}) => `Ausgaberegel von ${cards} entfernt`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index fea1fc048c1b..0a459f92172d 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7852,7 +7852,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e if (filters !== '') { text += ` ${filters}`; } - text += `sur ${cards}`; + text += ` sur ${cards}`; return text; }, removeRule: ({cards}: {cards: string}) => `a supprimé la règle de dépense de ${cards}`, diff --git a/src/languages/it.ts b/src/languages/it.ts index ebd2e3e27bef..12250d2f916a 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7811,7 +7811,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, if (filters !== '') { text += ` ${filters}`; } - text += `su ${cards}`; + text += ` su ${cards}`; return text; }, removeRule: ({cards}: {cards: string}) => `regola di spesa rimossa da ${cards}`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 672d734e5c09..ca10f439faee 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7781,7 +7781,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, if (filters !== '') { text += ` ${filters}`; } - text += `op ${cards}`; + text += ` op ${cards}`; return text; }, removeRule: ({cards}: {cards: string}) => `besteedregel verwijderd van ${cards}`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 9ed40d91466b..c157804f8ab1 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7771,7 +7771,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, if (filters !== '') { text += ` ${filters}`; } - text += `na ${cards}`; + text += ` na ${cards}`; return text; }, removeRule: ({cards}: {cards: string}) => `usunięto regułę wydatków z ${cards}`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index ee3a84e7c281..a3b76250236a 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7773,7 +7773,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, if (filters !== '') { text += ` ${filters}`; } - text += `em ${cards}`; + text += ` em ${cards}`; return text; }, removeRule: ({cards}: {cards: string}) => `removeu a regra de gasto de ${cards}`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 9190ed4175d5..6cab7221d9ed 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7570,7 +7570,7 @@ ${reportName} if (filters !== '') { text += ` ${filters}`; } - text += `在 ${cards}`; + text += ` 在 ${cards}`; return text; }, removeRule: ({cards}: {cards: string}) => `已从 ${cards} 中移除消费规则`, From b330a24ec8b7cd3556b42058941b588252b05536 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 27 May 2026 12:48:56 -0600 Subject: [PATCH 37/43] update copy --- src/languages/de.ts | 5 ++++- src/languages/es.ts | 5 ++++- src/languages/fr.ts | 5 ++++- src/languages/it.ts | 5 ++++- src/languages/ja.ts | 5 ++++- src/languages/nl.ts | 5 ++++- src/languages/pl.ts | 5 ++++- src/languages/pt-BR.ts | 5 ++++- src/languages/zh-hans.ts | 5 ++++- 9 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index c79e803fd42f..05dc4a7d68e3 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7829,7 +7829,10 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc restrictionVerb: {block: 'Block', allow: 'nur zulassen'}, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `Ausgaberegel von ${fromAction} zu ${toAction} auf ${cards} geändert`, - appliedToAdditionalCards: ({count}: {count: number}) => `Ausgaberegel auf ${count} weitere Karten angewendet`, + appliedToAdditionalCards: ({count}: {count: number}) => ({ + one: 'Ausgaberegel auf 1 weitere Karte angewendet', + other: `Ausgaberegel auf ${count} weitere Karten angewendet`, + }), phraseVerb: {added: 'hinzugefügt', removed: 'entfernt', changed: 'geändert', set: 'festlegen', applied: 'Angewendet'}, bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} Händler: „${value}“` : `Händler: „${value}“`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => diff --git a/src/languages/es.ts b/src/languages/es.ts index 79cc225930c3..140434c052a7 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7323,7 +7323,10 @@ ${amount} para ${merchant} - ${date}`, restrictionVerb: {block: 'bloquear', allow: 'permitir solo'}, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `cambió la regla de gasto de ${fromAction} a ${toAction} en ${cards}`, - appliedToAdditionalCards: ({count}: {count: number}) => `aplicó la regla de gasto a ${count} tarjetas adicionales`, + appliedToAdditionalCards: ({count}: {count: number}) => ({ + one: 'aplicó la regla de gasto a 1 tarjeta adicional', + other: `aplicó la regla de gasto a ${count} tarjetas adicionales`, + }), phraseVerb: {added: 'añadido', removed: 'eliminado', changed: 'cambió', set: 'establecer', applied: 'aplicado'}, bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} comercio «${value}»` : `comercio «${value}»`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 0a459f92172d..ff06e5da58b2 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7860,7 +7860,10 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `a modifié la règle de dépense de ${fromAction} à ${toAction} sur ${cards}`, - appliedToAdditionalCards: ({count}: {count: number}) => `règle de dépense appliquée à ${count} cartes supplémentaires`, + appliedToAdditionalCards: ({count}: {count: number}) => ({ + one: 'règle de dépense appliquée à 1 carte supplémentaire', + other: `règle de dépense appliquée à ${count} cartes supplémentaires`, + }), phraseVerb: {added: 'ajouté', removed: 'supprimé', changed: 'modifié', set: 'définir', applied: 'appliqué'}, bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} commerçant « ${value} »` : `commerçant « ${value} »`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => diff --git a/src/languages/it.ts b/src/languages/it.ts index 12250d2f916a..86d82cfd7335 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7819,7 +7819,10 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `ha modificato la regola di spesa da ${fromAction} a ${toAction} su ${cards}`, - appliedToAdditionalCards: ({count}: {count: number}) => `ha applicato la regola di spesa ad altre ${count} carte`, + appliedToAdditionalCards: ({count}: {count: number}) => ({ + one: 'ha applicato la regola di spesa a 1 altra carta', + other: `ha applicato la regola di spesa ad altre ${count} carte`, + }), phraseVerb: {added: 'aggiunto', removed: 'rimosso', changed: 'modificato', set: 'imposta', applied: 'applicata'}, bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} esercente '${value}'` : `esercente '${value}'`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => diff --git a/src/languages/ja.ts b/src/languages/ja.ts index b9cd6e332475..2df96831b64e 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7725,7 +7725,10 @@ ${reportName} restrictionVerb: {block: 'ブロック', allow: 'のみ許可'}, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `${cards} の利用ルールを ${fromAction} から ${toAction} に変更しました`, - appliedToAdditionalCards: ({count}: {count: number}) => `追加で ${count} 枚のカードに支出ルールを適用しました`, + appliedToAdditionalCards: ({count}: {count: number}) => ({ + one: '追加で 1 枚のカードに支出ルールを適用しました', + other: `追加で ${count} 枚のカードに支出ルールを適用しました`, + }), phraseVerb: {added: '追加しました', removed: '削除済み', changed: '変更しました', set: '設定', applied: '適用済み'}, bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective}なマーチャント「${value}」` : `加盟店「${value}」`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => diff --git a/src/languages/nl.ts b/src/languages/nl.ts index ca10f439faee..6366ea0817de 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7789,7 +7789,10 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `heeft bestedingsregel gewijzigd van ${fromAction} naar ${toAction} op ${cards}`, - appliedToAdditionalCards: ({count}: {count: number}) => `uitgavenregel toegepast op ${count} extra kaarten`, + appliedToAdditionalCards: ({count}: {count: number}) => ({ + one: 'uitgavenregel toegepast op 1 extra kaart', + other: `uitgavenregel toegepast op ${count} extra kaarten`, + }), phraseVerb: {added: 'toegevoegd', removed: 'verwijderd', changed: 'gewijzigd', set: 'instellen', applied: 'toegepast'}, bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} handelaar '${value}'` : `handelaar '${value}'`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => diff --git a/src/languages/pl.ts b/src/languages/pl.ts index c157804f8ab1..0f133333e658 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7778,7 +7778,10 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, restrictionVerb: {block: 'zablokuj', allow: 'zezwalaj tylko'}, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `zmieniono regułę wydatków z ${fromAction} na ${toAction} na ${cards}`, - appliedToAdditionalCards: ({count}: {count: number}) => `zastosowano regułę wydatków do ${count} dodatkowych kart`, + appliedToAdditionalCards: ({count}: {count: number}) => ({ + one: 'zastosowano regułę wydatków do 1 dodatkowej karty', + other: `zastosowano regułę wydatków do ${count} dodatkowych kart`, + }), phraseVerb: {added: 'dodano', removed: 'usunięto', changed: 'zmieniono', set: 'ustaw', applied: 'zastosowano'}, bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} sprzedawca '${value}'` : `sprzedawca „${value}”`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index a3b76250236a..9845a5956d49 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7781,7 +7781,10 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `alterou a regra de gasto de ${fromAction} para ${toAction} em ${cards}`, - appliedToAdditionalCards: ({count}: {count: number}) => `regra de gasto aplicada a mais ${count} cartões`, + appliedToAdditionalCards: ({count}: {count: number}) => ({ + one: 'regra de gasto aplicada a mais 1 cartão', + other: `regra de gasto aplicada a mais ${count} cartões`, + }), phraseVerb: {added: 'adicionado', removed: 'removido', changed: 'alterado', set: 'definir', applied: 'aplicado'}, bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `comerciante ${adjective} '${value}'` : `estabelecimento comercial '${value}'`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 6cab7221d9ed..4cde11e9c3d0 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7577,7 +7577,10 @@ ${reportName} restrictionVerb: {block: '阻止', allow: '仅允许'}, update: { modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => `已将 ${cards} 的消费规则从 ${fromAction} 更改为 ${toAction}`, - appliedToAdditionalCards: ({count}: {count: number}) => `已将支出规则应用于另外 ${count} 张卡片`, + appliedToAdditionalCards: ({count}: {count: number}) => ({ + one: '已将支出规则应用于另外 1 张卡片', + other: `已将支出规则应用于另外 ${count} 张卡片`, + }), phraseVerb: {added: '已添加', removed: '已移除', changed: '已更改', set: '设置', applied: '已应用'}, bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} 商家“${value}”` : `商户“${value}”`), bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => From 5427ca9e6dadc1afc5065060e2a85eb25735d20e Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 27 May 2026 13:22:24 -0600 Subject: [PATCH 38/43] add currency type --- src/types/onyx/OriginalMessage.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 74756ce547c5..07192ea23870 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -827,6 +827,9 @@ type OriginalMessageSpendRuleChangeLog = { /** Display name shown when the rule covers a single card */ displayName?: string; }>; + + /** Currency of the spend rule */ + currency?: string; }; /** Model of `join policy` report action */ From 35b113a0b09d716fe524a3aa9a18987d03420eaf Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 27 May 2026 16:55:30 -0600 Subject: [PATCH 39/43] add comment --- src/libs/SpendRuleChangeLogUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/SpendRuleChangeLogUtils.ts b/src/libs/SpendRuleChangeLogUtils.ts index e22232daf193..cc9f38fdecc7 100644 --- a/src/libs/SpendRuleChangeLogUtils.ts +++ b/src/libs/SpendRuleChangeLogUtils.ts @@ -11,7 +11,7 @@ import {getOriginalMessage, isActionOfType} from './ReportActionsUtils'; function getSpendRuleFallbackReportActionText(reportAction: OnyxEntry): string { const message = Array.isArray(reportAction?.message) ? reportAction?.message.at(0) : reportAction?.message; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - We intentionally use || here because empty strings from stripFollowupListFromHtml should also fall through to the text fallback const text = stripFollowupListFromHtml(message?.html) || (message?.text ?? ''); return text ? Parser.htmlToText(text) : ''; } From 93a3c6470a4dc9dfd66b29e3ea470b53eaa9aed2 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 28 May 2026 08:01:25 -0600 Subject: [PATCH 40/43] address comments --- src/libs/SpendRuleChangeLogUtils.ts | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/libs/SpendRuleChangeLogUtils.ts b/src/libs/SpendRuleChangeLogUtils.ts index cc9f38fdecc7..5d40a0d2aa0c 100644 --- a/src/libs/SpendRuleChangeLogUtils.ts +++ b/src/libs/SpendRuleChangeLogUtils.ts @@ -1,7 +1,7 @@ import type {OnyxEntry} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; +import {isSpendRuleCategory} from '@src/types/form/SpendRuleForm'; import type ReportAction from '@src/types/onyx/ReportAction'; import {convertAmountToDisplayString} from './CurrencyUtils'; import {formatList} from './Localize'; @@ -11,7 +11,8 @@ import {getOriginalMessage, isActionOfType} from './ReportActionsUtils'; function getSpendRuleFallbackReportActionText(reportAction: OnyxEntry): string { const message = Array.isArray(reportAction?.message) ? reportAction?.message.at(0) : reportAction?.message; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - We intentionally use || here because empty strings from stripFollowupListFromHtml should also fall through to the text fallback + // We intentionally use || here because empty strings from stripFollowupListFromHtml should also fall through to the text fallback + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const text = stripFollowupListFromHtml(message?.html) || (message?.text ?? ''); return text ? Parser.htmlToText(text) : ''; } @@ -41,7 +42,7 @@ function getSpendRuleAmountString(translate: LocalizedTranslate, amount: {operat if (amount.value.length === 0) { return ''; } - return translate('workspaceActions.expensifyCardRule.amountFilter', {operator: operatorWord, amount: formatSpendRuleAmount(amount, currency)}); + return translate('workspaceActions.expensifyCardRule.amountFilter', {operator: operatorWord, amount: formatSpendRuleAmount(amount.value, currency)}); } function getSpendRuleCardsSummary(translate: LocalizedTranslate, cards: ReadonlyArray<{displayName?: string}> | undefined): string { @@ -56,14 +57,12 @@ function getSpendRuleCardsSummary(translate: LocalizedTranslate, cards: Readonly } function getSpendRuleJoinFilters(items: readonly string[]): string { - const filtered = items.filter((value) => typeof value === 'string' && value !== ''); - return formatList(filtered); + return formatList(items.filter((value) => value !== '')); } function getSpendRuleCategoryDisplayName(translate: LocalizedTranslate, category: string): string { - const knownCategories = Object.values(CONST.SPEND_RULES.CATEGORIES) as string[]; - if (knownCategories.includes(category)) { - return translate(`workspace.rules.spendRules.categoryOptions.${category as ValueOf}`); + if (isSpendRuleCategory(category)) { + return translate(`workspace.rules.spendRules.categoryOptions.${category}`); } return category; } @@ -78,8 +77,8 @@ function getSpendRuleRestrictionVerb(translate: LocalizedTranslate, action: stri return action; } -function formatSpendRuleAmount(amount: {value: string[]}, currency: string): string { - return convertAmountToDisplayString(getSpendRuleValueInCents(amount.value), currency); +function formatSpendRuleAmount(amount: string[], currency: string): string { + return convertAmountToDisplayString(getSpendRuleValueInCents(amount), currency); } type SpendRuleStringDiff = {added: string[]; removed: string[]}; @@ -366,13 +365,13 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report ]; if (amountDiff.added.length === 1 && amountDiff.removed.length === 1) { - const oldValue = formatSpendRuleAmount(amountDiff.removed.at(0) ?? {value: []}, currency); - const newValue = formatSpendRuleAmount(amountDiff.added.at(0) ?? {value: []}, currency); + const oldValue = formatSpendRuleAmount(amountDiff.removed.at(0)?.value ?? [], currency); + const newValue = formatSpendRuleAmount(amountDiff.added.at(0)?.value ?? [], currency); const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountChange', {oldValue, newValue}); phrases.push({verb: 'changed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); } else { for (const amount of amountDiff.added) { - const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountSet', {value: formatSpendRuleAmount(amount, currency)}); + const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountSet', {value: formatSpendRuleAmount(amount.value, currency)}); phrases.push({verb: 'set', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); } if (amountDiff.removed.length > 0) { From f6bb1b43b9e918b6b1586a89d58004a4ac1fd530 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 29 May 2026 08:14:37 -0600 Subject: [PATCH 41/43] update language files, types --- src/languages/de.ts | 10 ++++++++-- src/languages/es.ts | 10 ++++++++-- src/languages/fr.ts | 10 ++++++++-- src/languages/it.ts | 10 ++++++++-- src/languages/ja.ts | 10 ++++++++-- src/languages/nl.ts | 10 ++++++++-- src/languages/pl.ts | 10 ++++++++-- src/languages/pt-BR.ts | 10 ++++++++-- src/languages/zh-hans.ts | 10 ++++++++-- src/libs/SpendRuleChangeLogUtils.ts | 18 ++++++++++-------- src/types/onyx/OriginalMessage.ts | 13 ++++++++----- 11 files changed, 90 insertions(+), 31 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 5c5ada6a7a17..4a0fb027dec7 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7837,7 +7837,10 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc amountOperator: {over: 'über', under: 'unter'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `Beträge ${operator} ${amount}`, theCard: 'die Karte', - multipleCards: ({count}: {count: number}) => `${count} Karten`, + multipleCards: ({count}: {count: number}) => ({ + one: '1 Karte', + other: `${count} Karten`, + }), addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { @@ -7865,7 +7868,10 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc bodyMaxAmount: 'Höchstbetrag', bodyMaxAmountSet: ({value}: {value: string}) => `Maximalbetrag bis ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `Maximalbetrag von ${oldValue} auf ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `Ausgabenregel für ${count} zusätzliche Karten`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => ({ + one: 'Ausgabenregel für 1 zusätzliche Karte', + other: `Ausgabenregel für ${count} zusätzliche Karten`, + }), bodyRemovedFromCards: ({cards}: {cards: string}) => `Ausgaberegel von ${cards}`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} auf ${cards}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} von ${cards}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index d0a5836abed2..d618fa3a6325 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7334,7 +7334,10 @@ ${amount} para ${merchant} - ${date}`, amountOperator: {over: 'más de', under: 'debajo'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `cantidades ${operator} ${amount}`, theCard: 'la tarjeta', - multipleCards: ({count}: {count: number}) => `${count} tarjetas`, + multipleCards: ({count}: {count: number}) => ({ + one: '1 tarjeta', + other: `${count} tarjetas`, + }), addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { @@ -7362,7 +7365,10 @@ ${amount} para ${merchant} - ${date}`, bodyMaxAmount: 'importe máximo', bodyMaxAmountSet: ({value}: {value: string}) => `importe máximo hasta ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `importe máximo de ${oldValue} a ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regla de gasto para ${count} tarjetas adicionales`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => ({ + one: 'regla de gasto para 1 tarjeta adicional', + other: `regla de gasto para ${count} tarjetas adicionales`, + }), bodyRemovedFromCards: ({cards}: {cards: string}) => `regla de gasto de ${cards}`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} en ${cards}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} de ${cards}`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 680d4f85f24a..3d8c7b1b78fc 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7867,7 +7867,10 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e amountOperator: {over: 'terminé', under: 'sous'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `montants ${operator} ${amount}`, theCard: 'la carte', - multipleCards: ({count}: {count: number}) => `${count} cartes`, + multipleCards: ({count}: {count: number}) => ({ + one: '1 carte', + other: `${count} cartes`, + }), addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { @@ -7896,7 +7899,10 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e bodyMaxAmount: 'montant maximal', bodyMaxAmountSet: ({value}: {value: string}) => `montant maximum à ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `montant maximal de ${oldValue} à ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `règle de dépense pour ${count} cartes supplémentaires`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => ({ + one: 'règle de dépense pour 1 carte supplémentaire', + other: `règle de dépense pour ${count} cartes supplémentaires`, + }), bodyRemovedFromCards: ({cards}: {cards: string}) => `règle de dépense provenant de ${cards}`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} sur ${cards}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} de ${cards}`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 8d364834fb9a..a8d6bf06c49c 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7825,7 +7825,10 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, amountOperator: {over: 'terminato', under: 'sotto'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `importi ${operator} ${amount}`, theCard: 'la carta', - multipleCards: ({count}: {count: number}) => `${count} carte`, + multipleCards: ({count}: {count: number}) => ({ + one: '1 carta', + other: `${count} carte`, + }), addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { @@ -7854,7 +7857,10 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, bodyMaxAmount: 'importo massimo', bodyMaxAmountSet: ({value}: {value: string}) => `importo massimo a ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `importo massimo da ${oldValue} a ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regola di spesa per ${count} carte aggiuntive`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => ({ + one: 'regola di spesa per 1 carta aggiuntiva', + other: `regola di spesa per ${count} carte aggiuntive`, + }), bodyRemovedFromCards: ({cards}: {cards: string}) => `regola di spesa da ${cards}`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} su ${cards}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} da ${cards}`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index a1664bade535..697e537fee7f 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7736,7 +7736,10 @@ ${reportName} }, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `金額が ${amount} を${operator}`, theCard: 'カード', - multipleCards: ({count}: {count: number}) => `${count} 枚のカード`, + multipleCards: ({count}: {count: number}) => ({ + one: '1 枚のカード', + other: `${count} 枚のカード`, + }), addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { @@ -7763,7 +7766,10 @@ ${reportName} bodyMaxAmount: '最大金額', bodyMaxAmountSet: ({value}: {value: string}) => `最大金額を${value}に設定`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `最大金額を${oldValue}から${newValue}に変更しました`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `${count}枚の追加カードに支出ルールを適用します`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => ({ + one: '1枚の追加カードに支出ルールを適用します', + other: `${count}枚の追加カードに支出ルールを適用します`, + }), bodyRemovedFromCards: ({cards}: {cards: string}) => `${cards} からの支出ルール`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${cards} 上の ${content}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `${cards} からの ${content}`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index f25a8612d8d1..3598e0732058 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7795,7 +7795,10 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, amountOperator: {over: 'over', under: 'onder'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `bedragen ${operator} ${amount}`, theCard: 'de kaart', - multipleCards: ({count}: {count: number}) => `${count} kaarten`, + multipleCards: ({count}: {count: number}) => ({ + one: '1 kaart', + other: `${count} kaarten`, + }), addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { @@ -7824,7 +7827,10 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, bodyMaxAmount: 'maximum bedrag', bodyMaxAmountSet: ({value}: {value: string}) => `maximumbedrag tot ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `maximumbedrag van ${oldValue} naar ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `bestedingsregel naar ${count} extra kaarten`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => ({ + one: 'bestedingsregel naar 1 extra kaart', + other: `bestedingsregel naar ${count} extra kaarten`, + }), bodyRemovedFromCards: ({cards}: {cards: string}) => `bestedingsregel van ${cards}`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} op ${cards}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} van ${cards}`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 78228422e6fd..a80b7d389a9a 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7785,7 +7785,10 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, amountOperator: {over: 'ponad', under: 'pod'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `kwoty ${operator} ${amount}`, theCard: 'karta', - multipleCards: ({count}: {count: number}) => `${count} karty`, + multipleCards: ({count}: {count: number}) => ({ + one: '1 karta', + other: `${count} karty`, + }), addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { @@ -7813,7 +7816,10 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, bodyMaxAmount: 'maksymalna kwota', bodyMaxAmountSet: ({value}: {value: string}) => `maksymalna kwota do ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `maksymalną kwotę z ${oldValue} na ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `zasada wydatków dla ${count} dodatkowych kart`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => ({ + one: 'zasada wydatków dla 1 dodatkowej karty', + other: `zasada wydatków dla ${count} dodatkowych kart`, + }), bodyRemovedFromCards: ({cards}: {cards: string}) => `zasada wydatków z ${cards}`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} na ${cards}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} z ${cards}`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 017506f65edf..5067e0a35112 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7787,7 +7787,10 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, amountOperator: {over: 'acima', under: 'abaixo'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `valores ${operator} ${amount}`, theCard: 'o cartão', - multipleCards: ({count}: {count: number}) => `${count} cartões`, + multipleCards: ({count}: {count: number}) => ({ + one: '1 cartão', + other: `${count} cartões`, + }), addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { @@ -7816,7 +7819,10 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, bodyMaxAmount: 'valor máximo', bodyMaxAmountSet: ({value}: {value: string}) => `valor máximo até ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `valor máximo de ${oldValue} para ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regra de gasto para mais ${count} cartões`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => ({ + one: 'regra de gasto para mais 1 cartão', + other: `regra de gasto para mais ${count} cartões`, + }), bodyRemovedFromCards: ({cards}: {cards: string}) => `regra de gasto de ${cards}`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${content} em ${cards}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} de ${cards}`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 331d9c6059d8..4a4e860e55d8 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7587,7 +7587,10 @@ ${reportName} amountOperator: {over: '结束', under: '在……之下'}, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `金额 ${operator} ${amount}`, theCard: '该卡', - multipleCards: ({count}: {count: number}) => `${count} 张卡片`, + multipleCards: ({count}: {count: number}) => ({ + one: '1 张卡片', + other: `${count} 张卡片`, + }), addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { let text = verb; if (filters !== '') { @@ -7614,7 +7617,10 @@ ${reportName} bodyMaxAmount: '最大金额', bodyMaxAmountSet: ({value}: {value: string}) => `最大金额至 ${value}`, bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `最大金额从 ${oldValue} 变为 ${newValue}`, - bodyAppliedToAdditionalCards: ({count}: {count: number}) => `将消费规则应用到另外 ${count} 张卡片`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => ({ + one: '将消费规则应用到另外 1 张卡片', + other: `将消费规则应用到另外 ${count} 张卡片`, + }), bodyRemovedFromCards: ({cards}: {cards: string}) => `来自 ${cards} 的消费规则`, composeOnCards: ({content, cards}: {content: string; cards: string}) => `${cards} 上的 ${content}`, composeFromCards: ({content, cards}: {content: string; cards: string}) => `来自 ${cards} 的 ${content}`, diff --git a/src/libs/SpendRuleChangeLogUtils.ts b/src/libs/SpendRuleChangeLogUtils.ts index 5d40a0d2aa0c..3ba01c4db3bd 100644 --- a/src/libs/SpendRuleChangeLogUtils.ts +++ b/src/libs/SpendRuleChangeLogUtils.ts @@ -1,4 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; import {isSpendRuleCategory} from '@src/types/form/SpendRuleForm'; @@ -11,13 +12,14 @@ import {getOriginalMessage, isActionOfType} from './ReportActionsUtils'; function getSpendRuleFallbackReportActionText(reportAction: OnyxEntry): string { const message = Array.isArray(reportAction?.message) ? reportAction?.message.at(0) : reportAction?.message; + // We intentionally use || here because empty strings from stripFollowupListFromHtml should also fall through to the text fallback // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const text = stripFollowupListFromHtml(message?.html) || (message?.text ?? ''); return text ? Parser.htmlToText(text) : ''; } -function getSpendRuleActionVerb(translate: LocalizedTranslate, action: string): string { +function getSpendRuleActionVerb(translate: LocalizedTranslate, action: ValueOf): string { if (action === CONST.SPEND_RULES.ACTION.BLOCK) { return translate('workspaceActions.expensifyCardRule.actionVerb.block'); } @@ -27,7 +29,7 @@ function getSpendRuleActionVerb(translate: LocalizedTranslate, action: string): return ''; } -function getSpendRuleAmountOperatorWord(translate: LocalizedTranslate, operator: string): string { +function getSpendRuleAmountOperatorWord(translate: LocalizedTranslate, operator: ValueOf): string { if (operator === CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO) { return translate('workspaceActions.expensifyCardRule.amountOperator.under'); } @@ -37,11 +39,11 @@ function getSpendRuleAmountOperatorWord(translate: LocalizedTranslate, operator: return ''; } -function getSpendRuleAmountString(translate: LocalizedTranslate, amount: {operator: string; value: string[]}, currency: string): string { - const operatorWord = getSpendRuleAmountOperatorWord(translate, amount.operator); +function getSpendRuleAmountString(translate: LocalizedTranslate, amount: {operator: ValueOf; value: string[]}, currency: string): string { if (amount.value.length === 0) { return ''; } + const operatorWord = getSpendRuleAmountOperatorWord(translate, amount.operator); return translate('workspaceActions.expensifyCardRule.amountFilter', {operator: operatorWord, amount: formatSpendRuleAmount(amount.value, currency)}); } @@ -60,14 +62,14 @@ function getSpendRuleJoinFilters(items: readonly string[]): string { return formatList(items.filter((value) => value !== '')); } -function getSpendRuleCategoryDisplayName(translate: LocalizedTranslate, category: string): string { +function getSpendRuleCategoryDisplayName(translate: LocalizedTranslate, category: ValueOf): string { if (isSpendRuleCategory(category)) { return translate(`workspace.rules.spendRules.categoryOptions.${category}`); } return category; } -function getSpendRuleRestrictionVerb(translate: LocalizedTranslate, action: string): string { +function getSpendRuleRestrictionVerb(translate: LocalizedTranslate, action: ValueOf): string { if (action === CONST.SPEND_RULES.ACTION.BLOCK) { return translate('workspaceActions.expensifyCardRule.restrictionVerb.block'); } @@ -91,7 +93,7 @@ function computeSpendRuleStringDiff(oldValues: string[], newValues: string[]): S return {added, removed}; } -type SpendRuleAmount = {operator: string; value: string[]}; +type SpendRuleAmount = {operator: ValueOf; value: string[]}; type SpendRuleAmountDiff = {added: SpendRuleAmount[]; removed: SpendRuleAmount[]}; function getSpendRuleValueInCents(value: string[]): number { @@ -301,7 +303,7 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report const message = getOriginalMessage(reportAction) ?? {}; const oldAction = message.oldAction ?? CONST.SPEND_RULES.ACTION.ALLOW; const newAction = message.action ?? CONST.SPEND_RULES.ACTION.ALLOW; - const actionChanged = oldAction !== '' && oldAction !== newAction; + const actionChanged = oldAction !== newAction; const currency = message.currency ?? CONST.CURRENCY.USD; const oldMerchants = message.oldMerchants ?? []; diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 07192ea23870..b2d3ab8d7b11 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -772,13 +772,16 @@ type OriginalMessagePolicyChangeLog = { didJoinPolicy?: boolean; }; +/** Amount operators for spend rules */ +type SpendRuleAmountOperator = typeof CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN | typeof CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO; + /** Model of an Expensify card spend rule change log action (add, update, or remove) */ type OriginalMessageSpendRuleChangeLog = { - /** Spend rule action (`block` or `allow`) */ - action?: string; + /** Spend rule action */ + action?: ValueOf; /** Previous spend rule action when the rule's restriction type was updated */ - oldAction?: string; + oldAction?: ValueOf; /** Merchants included in a spend rule */ merchants?: string[]; @@ -795,7 +798,7 @@ type OriginalMessageSpendRuleChangeLog = { /** Max-amount filters in a spend rule */ amounts?: Array<{ /** Operator (`gt` for "over", `lte` for "under") */ - operator: string; + operator: SpendRuleAmountOperator; /** Amount value as a decimal dollar string array (e.g. `['100.40']`) */ value: string[]; @@ -804,7 +807,7 @@ type OriginalMessageSpendRuleChangeLog = { /** Previous list of max-amount filters when a spend rule was updated */ oldAmounts?: Array<{ /** Operator (`gt` for "over", `lte` for "under") */ - operator: string; + operator: SpendRuleAmountOperator; /** Amount value as a decimal dollar string array (e.g. `['100.40']`) */ value: string[]; From ee635e2a504df668002e4a8c9b11fd8158388ad2 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 29 May 2026 08:17:56 -0600 Subject: [PATCH 42/43] fix types --- src/libs/SpendRuleChangeLogUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/SpendRuleChangeLogUtils.ts b/src/libs/SpendRuleChangeLogUtils.ts index 3ba01c4db3bd..f5a3f63f93a7 100644 --- a/src/libs/SpendRuleChangeLogUtils.ts +++ b/src/libs/SpendRuleChangeLogUtils.ts @@ -62,7 +62,7 @@ function getSpendRuleJoinFilters(items: readonly string[]): string { return formatList(items.filter((value) => value !== '')); } -function getSpendRuleCategoryDisplayName(translate: LocalizedTranslate, category: ValueOf): string { +function getSpendRuleCategoryDisplayName(translate: LocalizedTranslate, category: string): string { if (isSpendRuleCategory(category)) { return translate(`workspace.rules.spendRules.categoryOptions.${category}`); } @@ -346,7 +346,7 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report } const adjective: SpendRulePhraseAdjective = newAction === CONST.SPEND_RULES.ACTION.BLOCK || newAction === CONST.SPEND_RULES.ACTION.ALLOW ? newAction : ''; - const adjectiveWord = getSpendRuleActionVerb(translate, adjective); + const adjectiveWord = adjective === '' ? '' : getSpendRuleActionVerb(translate, adjective); const phrases: SpendRulePhrase[] = [ ...getDiffPhrases( merchantDiff, From 83506988af90012a63a1415cd8c15fcccd863457 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 29 May 2026 08:28:26 -0600 Subject: [PATCH 43/43] create cardID type, fix Onyx import lint --- src/libs/SpendRuleChangeLogUtils.ts | 3 ++- src/types/onyx/Card.ts | 30 ++++++++++++++++------------- src/types/onyx/OriginalMessage.ts | 5 +++-- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/libs/SpendRuleChangeLogUtils.ts b/src/libs/SpendRuleChangeLogUtils.ts index f5a3f63f93a7..d1ed4b13ec08 100644 --- a/src/libs/SpendRuleChangeLogUtils.ts +++ b/src/libs/SpendRuleChangeLogUtils.ts @@ -3,6 +3,7 @@ import type {ValueOf} from 'type-fest'; import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; import {isSpendRuleCategory} from '@src/types/form/SpendRuleForm'; +import type {CardID} from '@src/types/onyx/Card'; import type ReportAction from '@src/types/onyx/ReportAction'; import {convertAmountToDisplayString} from './CurrencyUtils'; import {formatList} from './Localize'; @@ -123,7 +124,7 @@ function computeSpendRuleAmountDiff(oldAmounts: SpendRuleAmount[], newAmounts: S }; } -type SpendRuleCard = {cardID?: number | string; displayName?: string}; +type SpendRuleCard = {cardID?: CardID; displayName?: string}; type SpendRuleCardDiff = {added: SpendRuleCard[]; removed: SpendRuleCard[]}; function getSpendRuleCardID(card: SpendRuleCard): number | undefined { diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index 566fe47ab3fe..ac6873101ede 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -2,9 +2,12 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; import type {SpendRuleCategory} from '@src/types/form/SpendRuleForm'; import type {CardFeedWithNumber} from './CardFeeds'; -import type * as OnyxCommon from './OnyxCommon'; +import type {ErrorFields, Errors, OnyxValueWithOfflineFeedback, PendingAction} from './OnyxCommon'; import type PersonalDetails from './PersonalDetails'; +/** Card identifier */ +type CardID = number | string; + /** Model of Expensify card status changes */ type CardStatusChanges = { /** Card status change date */ @@ -42,7 +45,7 @@ type PossibleFraudData = { }; /** Model of Expensify card */ -type Card = OnyxCommon.OnyxValueWithOfflineFeedback<{ +type Card = OnyxValueWithOfflineFeedback<{ /** Card ID number */ cardID: number; @@ -110,10 +113,10 @@ type Card = OnyxCommon.OnyxValueWithOfflineFeedback<{ lastImportAttempt?: string; /** Card related error messages */ - errors?: OnyxCommon.Errors; + errors?: Errors; /** Collection of form field errors */ - errorFields?: OnyxCommon.ErrorFields; + errorFields?: ErrorFields; /** Is card data loading */ isLoading?: boolean; @@ -128,7 +131,7 @@ type Card = OnyxCommon.OnyxValueWithOfflineFeedback<{ isOfflinePINMarket?: boolean; /** Additional card data */ - nameValuePairs?: OnyxCommon.OnyxValueWithOfflineFeedback<{ + nameValuePairs?: OnyxValueWithOfflineFeedback<{ /** Type of card spending limits */ limitType?: ValueOf; @@ -195,10 +198,10 @@ type Card = OnyxCommon.OnyxValueWithOfflineFeedback<{ isPINBlocked?: boolean; /** Collection of errors coming from BE */ - errors?: OnyxCommon.Errors; + errors?: Errors; /** Collection of form field errors */ - errorFields?: OnyxCommon.ErrorFields; + errorFields?: ErrorFields; /** * Metadata about when and by whom the card was frozen. @@ -209,7 +212,7 @@ type Card = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Possible fraud information */ possibleFraud?: PossibleFraudData; }> & - OnyxCommon.OnyxValueWithOfflineFeedback< + OnyxValueWithOfflineFeedback< /** Type of export card */ Record | ValueOf, string> >; @@ -239,7 +242,7 @@ type ProvisioningCardData = { isLoading?: boolean; /** Error message */ - errors?: OnyxCommon.Errors; + errors?: Errors; /** User's address, required to add card to wallet */ userAddress: { @@ -387,7 +390,7 @@ type IssueNewCard = { isLoading?: boolean; /** Error message */ - errors?: OnyxCommon.Errors; + errors?: Errors; /** Whether the request was successful */ isSuccessful?: boolean; @@ -422,15 +425,15 @@ type CardAssignmentData = { cardholder?: PersonalDetails | null; /** Errors */ - errors?: OnyxCommon.Errors; + errors?: Errors; /** * */ - errorFields?: OnyxCommon.ErrorFields; + errorFields?: ErrorFields; /** Pending action */ - pendingAction?: OnyxCommon.PendingAction; + pendingAction?: PendingAction; }; /** @@ -446,6 +449,7 @@ type FrozenCardData = { export default Card; export type { + CardID, ExpensifyCardDetails, CardList, IssueNewCard, diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index b2d3ab8d7b11..6f137d79f51f 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -1,6 +1,7 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import type {CardID} from './Card'; import type {PolicyRuleTaxRate} from './ExpenseRule'; import type {Attendee} from './IOU'; import type {OldDotOriginalMessageMap} from './OldDotAction'; @@ -816,7 +817,7 @@ type OriginalMessageSpendRuleChangeLog = { /** Cards a spend rule is scoped to */ cards?: Array<{ /** Card identifier */ - cardID: number | string; + cardID: CardID; /** Display name shown when the rule covers a single card */ displayName?: string; @@ -825,7 +826,7 @@ type OriginalMessageSpendRuleChangeLog = { /** Previous list of cards when a spend rule's card scope was updated */ oldCards?: Array<{ /** Card identifier */ - cardID: number | string; + cardID: CardID; /** Display name shown when the rule covers a single card */ displayName?: string;