diff --git a/.eslintrc.js b/.eslintrc.js index d9e25cc596f7..761a62b8314b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -108,7 +108,7 @@ module.exports = { 'plugin:you-dont-need-lodash-underscore/all', 'plugin:prettier/recommended', ], - plugins: ['@typescript-eslint', 'jsdoc', 'you-dont-need-lodash-underscore', 'react-native-a11y', 'react', 'testing-library', 'eslint-plugin-react-compiler', 'lodash'], + plugins: ['@typescript-eslint', 'jsdoc', 'you-dont-need-lodash-underscore', 'react-native-a11y', 'react', 'testing-library', 'eslint-plugin-react-compiler', 'lodash', 'deprecation'], ignorePatterns: ['lib/**'], parser: '@typescript-eslint/parser', parserOptions: { @@ -177,6 +177,7 @@ module.exports = { // ESLint core rules 'es/no-nullish-coalescing-operators': 'off', 'es/no-optional-chaining': 'off', + 'deprecation/deprecation': 'off', // Import specific rules 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], diff --git a/README.md b/README.md index c8faff111bae..4a691045e7c2 100644 --- a/README.md +++ b/README.md @@ -619,7 +619,30 @@ Some pointers: key to the translation file and use the arrow function version, like so: `nameOfTheKey: ({amount, dateTime}) => "User has sent " + amount + " to you on " + dateTime,`. This is because the order of the phrases might vary from one language to another. - +- When working with translations that involve plural forms, it's important to handle different cases correctly. + + For example: + - zero: Used when there are no items **(optional)**. + - one: Used when there's exactly one item. + - two: Used when there's two items. **(optional)** + - few: Used for a small number of items **(optional)**. + - many: Used for larger quantities **(optional)**. + - other: A catch-all case for other counts or variations. + + Here’s an example of how to implement plural translations: + + messages: () => ({ + zero: 'No messages', + one: 'One message', + two: 'Two messages', + few: (count) => `${count} messages`, + many: (count) => `You have ${count} messages`, + other: (count) => `You have ${count} unread messages`, + }) + + In your code, you can use the translation like this: + + `translate('common.messages', {count: 1});` ---- # Deploying diff --git a/android/app/build.gradle b/android/app/build.gradle index 833f8290e5e6..9153966e1d5d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009004001 - versionName "9.0.40-1" + versionCode 1009004004 + versionName "9.0.40-4" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/table.svg b/assets/images/table.svg index a9cfe68f339e..36d4ced774f1 100644 --- a/assets/images/table.svg +++ b/assets/images/table.svg @@ -1,3 +1,3 @@ - + diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Enable-Global-Reimbursements.md b/docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Enable-Global-Reimbursements.md index b0c767fce277..37d8d8bbe42b 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Enable-Global-Reimbursements.md +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Enable-Global-Reimbursements.md @@ -90,6 +90,10 @@ Have the employee double-check that their [default workspace](https://help.expen - **Authorized User**: The person who will process global reimbursements. The Authorized User should be the same person who manages the bank account connection in Expensify. - **User**: You can leave this section blank because the “User” is Expensify. +**Does Global Reimbursement support Sepa in the EU?** + +Global Reimbursement uses Sepa B2B to facilitate payments from EU-based accounts. Sepa Core is not supported. + {% include faq-end.md %} diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md index 1fb1b09328b9..bda84eb0a49f 100644 --- a/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md +++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md @@ -26,9 +26,7 @@ To connect QuickBooks Desktop to Expensify, you must log into QuickBooks Desktop 7. Download the Web Connector and go through the guided installation process. 8. Open the Web Connector. -9. Click on **Add an Application**. - - ![The Web Connnector Pop-up where you will need to click on Add an Application](https://help.expensify.com/assets/images/QBO_desktop_03.png){:width="100%"} +9. Download the config file when prompted during the setup process, then open it using your File Explorer. This will automatically load the application into the QuickBooks Web Connector. {% include info.html %} For this step, it is key to ensure that the correct company file is open in QuickBooks Desktop and that it is the only one open. diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg index 78fb5d53d9e9..d2f181f6b7f4 100644 Binary files a/ios/NewApp_AdHoc.mobileprovision.gpg and b/ios/NewApp_AdHoc.mobileprovision.gpg differ diff --git a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg index ac2c76a118d5..c0afa40ecb29 100644 Binary files a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg and b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg differ diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 768062717d4b..1a29a275b956 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -43,7 +43,7 @@ D27CE6B77196EF3EF450EEAC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0D3F9E814828D91464DF9D35 /* PrivacyInfo.xcprivacy */; }; DD79042B2792E76D004484B4 /* RCTBootSplash.mm in Sources */ = {isa = PBXBuildFile; fileRef = DD79042A2792E76D004484B4 /* RCTBootSplash.mm */; }; DDCB2E57F334C143AC462B43 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D20D83B0E39BA6D21761E72 /* ExpoModulesProvider.swift */; }; - E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; + E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */ = {isa = PBXBuildFile; }; E9DF872D2525201700607FDC /* AirshipConfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = E9DF872C2525201700607FDC /* AirshipConfig.plist */; }; ED222ED90E074A5481A854FA /* ExpensifyNeue-BoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */; }; F0C450EA2705020500FD2970 /* colors.json in Resources */ = {isa = PBXBuildFile; fileRef = F0C450E92705020500FD2970 /* colors.json */; }; @@ -131,6 +131,7 @@ 7F3784A52C7512CF00063508 /* NewExpensifyReleaseDevelopment.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NewExpensifyReleaseDevelopment.entitlements; path = NewExpensify/NewExpensifyReleaseDevelopment.entitlements; sourceTree = ""; }; 7F3784A62C7512D900063508 /* NewExpensifyReleaseAdHoc.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NewExpensifyReleaseAdHoc.entitlements; path = NewExpensify/NewExpensifyReleaseAdHoc.entitlements; sourceTree = ""; }; 7F3784A72C75131000063508 /* NewExpensifyReleaseProduction.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NewExpensifyReleaseProduction.entitlements; path = NewExpensify/NewExpensifyReleaseProduction.entitlements; sourceTree = ""; }; + 7F9C91352CA5EC4900FC4DC1 /* NotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationServiceExtension.entitlements; sourceTree = ""; }; 7F9DD8D92B2A445B005E3AFA /* ExpError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpError.swift; sourceTree = ""; }; 7FD73C9B2B23CE9500420AF3 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7FD73C9D2B23CE9500420AF3 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; @@ -175,8 +176,8 @@ buildActionMask = 2147483647; files = ( 383643682B6D4AE2005BB9AE /* DeviceCheck.framework in Frameworks */, - E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, - E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, + E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */, + E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */, 8744C5400E24E379441C04A4 /* libPods-NewExpensify.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -266,6 +267,7 @@ 7FD73C9C2B23CE9500420AF3 /* NotificationServiceExtension */ = { isa = PBXGroup; children = ( + 7F9C91352CA5EC4900FC4DC1 /* NotificationServiceExtension.entitlements */, 7FD73C9D2B23CE9500420AF3 /* NotificationService.swift */, 7FD73C9F2B23CE9500420AF3 /* Info.plist */, 7F9DD8D92B2A445B005E3AFA /* ExpError.swift */, @@ -1183,6 +1185,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; @@ -1347,6 +1350,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; @@ -1433,6 +1437,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; @@ -1518,8 +1523,9 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; @@ -1560,7 +1566,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.chat.expensify.chat.NotificationServiceExtension; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "(NewApp) Development: Notification Service"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "(NewApp) AppStore: Notification Service"; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; @@ -1604,6 +1610,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; @@ -1683,6 +1690,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; @@ -1761,6 +1769,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 4eeb658f3347..af696a13c998 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.40.1 + 9.0.40.4 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 7dc1b1416139..0795209286ed 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.40.1 + 9.0.40.4 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 83fa9ece1deb..a545bd82c164 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.40 CFBundleVersion - 9.0.40.1 + 9.0.40.4 NSExtension NSExtensionPointIdentifier diff --git a/ios/NotificationServiceExtension/NotificationServiceExtension.entitlements b/ios/NotificationServiceExtension/NotificationServiceExtension.entitlements new file mode 100644 index 000000000000..f52d3207d6e3 --- /dev/null +++ b/ios/NotificationServiceExtension/NotificationServiceExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.expensify.new + + + diff --git a/package-lock.json b/package-lock.json index 6af9c1981bc8..d43c8fee25c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.40-1", + "version": "9.0.40-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.40-1", + "version": "9.0.40-4", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -50,7 +50,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.84", + "expensify-common": "2.0.88", "expo": "51.0.31", "expo-av": "14.0.7", "expo-image": "1.12.15", @@ -24037,9 +24037,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.84", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.84.tgz", - "integrity": "sha512-VistjMexRz/1u1IqjIZwGRE7aS6QOat7420Dualn+NaqMHGkfeeB4uUR3RQhCtlDbcwFBKTryIGgSrrC0N1YpA==", + "version": "2.0.88", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.88.tgz", + "integrity": "sha512-4k6X6BekydYSRWkWRMB/Ts0W5Zx3BskEpLQEuxpq+cW9QIvTyFliho/dMLaXYOqS6nMQuzkjJYqfGPx9agVnOg==", "dependencies": { "awesome-phonenumber": "^5.4.0", "classnames": "2.5.0", diff --git a/package.json b/package.json index deff4054ce01..e8322960c61c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.40-1", + "version": "9.0.40-4", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -107,7 +107,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.84", + "expensify-common": "2.0.88", "expo": "51.0.31", "expo-av": "14.0.7", "expo-image": "1.12.15", diff --git a/src/CONST.ts b/src/CONST.ts index 33eae01ed9c8..4ca9b45f13df 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -171,7 +171,7 @@ const CONST = { }, // Note: Group and Self-DM excluded as these are not tied to a Workspace - WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT], + WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT, chatTypes.INVOICE], ANDROID_PACKAGE_NAME, WORKSPACE_ENABLE_FEATURE_REDIRECT_DELAY: 100, ANIMATED_HIGHLIGHT_ENTRY_DELAY: 50, @@ -719,7 +719,9 @@ const CONST = { PRICING: `https://www.expensify.com/pricing`, COMPANY_CARDS_HELP: 'https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Commercial-Card-Feeds', CUSTOM_REPORT_NAME_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates', + CONFIGURE_REIMBURSEMENT_SETTINGS_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/workspaces/Configure-Reimbursement-Settings', COPILOT_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Assign-or-remove-a-Copilot', + DELAYED_SUBMISSION_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/reports/Automatically-submit-employee-reports', // Use Environment.getEnvironmentURL to get the complete URL with port number DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:', OLDDOT_URLS: { @@ -1009,6 +1011,7 @@ const CONST = { MAX_PREVIEW_AVATARS: 4, MAX_ROOM_NAME_LENGTH: 99, LAST_MESSAGE_TEXT_MAX_LENGTH: 200, + MIN_LENGTH_LAST_MESSAGE_WITH_ELLIPSIS: 20, OWNER_EMAIL_FAKE: '__FAKE__', OWNER_ACCOUNT_ID_FAKE: 0, DEFAULT_REPORT_NAME: 'Chat Report', @@ -2183,7 +2186,7 @@ const CONST = { AUTO_REIMBURSEMENT_MAX_LIMIT_CENTS: 2000000, AUTO_REIMBURSEMENT_DEFAULT_LIMIT_CENTS: 10000, AUTO_APPROVE_REPORTS_UNDER_DEFAULT_CENTS: 10000, - RANDOM_AUDIT_DEFAULT_PERCENTAGE: 5, + RANDOM_AUDIT_DEFAULT_PERCENTAGE: 0.05, AUTO_REPORTING_FREQUENCIES: { INSTANT: 'instant', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 5d4c2f40c519..51d05870da2e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -35,7 +35,7 @@ const ROUTES = { SEARCH_CENTRAL_PANE: { route: 'search', - getRoute: ({query}: {query: SearchQueryString}) => `search?q=${encodeURIComponent(query)}` as const, + getRoute: ({query, name}: {query: SearchQueryString; name?: string}) => `search?q=${encodeURIComponent(query)}${name ? `&name=${name}` : ''}` as const, }, SEARCH_SAVED_SEARCH_RENAME: { route: 'search/saved-search/rename', @@ -287,7 +287,8 @@ const ROUTES = { }, EDIT_REPORT_FIELD_REQUEST: { route: 'r/:reportID/edit/policyField/:policyID/:fieldID', - getRoute: (reportID: string, policyID: string, fieldID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/edit/policyField/${policyID}/${fieldID}` as const, backTo), + getRoute: (reportID: string, policyID: string, fieldID: string, backTo?: string) => + getUrlWithBackToParam(`r/${reportID}/edit/policyField/${policyID}/${encodeURIComponent(fieldID)}` as const, backTo), }, REPORT_WITH_ID_DETAILS_SHARE_CODE: { route: 'r/:reportID/details/shareCode', @@ -382,9 +383,13 @@ const ROUTES = { }, }, MONEY_REQUEST_HOLD_REASON: { - route: ':type/edit/reason/:transactionID?', - getRoute: (type: ValueOf, transactionID: string, reportID: string, backTo: string) => - `${type}/edit/reason/${transactionID}?backTo=${backTo}&reportID=${reportID}` as const, + route: ':type/edit/reason/:transactionID?/:searchHash?', + getRoute: (type: ValueOf, transactionID: string, reportID: string, backTo: string, searchHash?: number) => { + const route = searchHash + ? (`${type}/edit/reason/${transactionID}/${searchHash}/?backTo=${backTo}&reportID=${reportID}` as const) + : (`${type}/edit/reason/${transactionID}/?backTo=${backTo}&reportID=${reportID}` as const); + return route; + }, }, MONEY_REQUEST_CREATE: { route: ':action/:iouType/start/:transactionID/:reportID', diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx index 71970b88eac9..8d3e311c7c61 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -108,7 +108,7 @@ function AccountSwitcher() { const error = ErrorUtils.getLatestErrorField({errorFields}, 'connect'); const personalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(email); return createBaseMenuItem(personalDetails, error, { - badgeText: translate('delegate.role', role), + badgeText: translate('delegate.role', {role}), onPress: () => { if (isOffline) { Modal.close(() => setShouldShowOfflineModal(true)); diff --git a/src/components/AccountingConnectionConfirmationModal.tsx b/src/components/AccountingConnectionConfirmationModal.tsx index c472f215b6df..bfacd8c0bf76 100644 --- a/src/components/AccountingConnectionConfirmationModal.tsx +++ b/src/components/AccountingConnectionConfirmationModal.tsx @@ -14,11 +14,11 @@ function AccountingConnectionConfirmationModal({integrationToConnect, onCancel, return ( diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 353bacdc0a25..322f28aa246d 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -187,7 +187,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti shiftHorizontal={variables.gbrTooltipShiftHorizontal} shiftVertical={variables.composerTooltipShiftVertical} wrapperStyle={styles.quickActionTooltipWrapper} - onPressOverlay={() => User.dismissGBRTooltip()} + onHideTooltip={() => User.dismissGBRTooltip()} > diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index 383784a468d7..b677cf1e66e2 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -8,7 +8,7 @@ import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import * as Localize from '@libs/Localize'; import * as NumberFormatUtils from '@libs/NumberFormatUtils'; import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; +import type {TranslationParameters, TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {WithCurrentUserPersonalDetailsProps} from './withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from './withCurrentUserPersonalDetails'; @@ -28,7 +28,7 @@ type LocaleContextProviderProps = LocaleContextProviderOnyxProps & type LocaleContextProps = { /** Returns translated string for given locale and phrase */ - translate: (phraseKey: TKey, ...phraseParameters: Localize.PhraseParameters>) => string; + translate: (path: TPath, ...parameters: TranslationParameters) => string; /** Formats number formatted according to locale and options */ numberFormat: (number: number, options?: Intl.NumberFormatOptions) => string; @@ -79,8 +79,8 @@ function LocaleContextProvider({preferredLocale, currentUserPersonalDetails, chi const translate = useMemo( () => - (phraseKey, ...phraseParameters) => - Localize.translate(locale, phraseKey, ...phraseParameters), + (path, ...parameters) => + Localize.translate(locale, path, ...parameters), [locale], ); diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 2524658d6ffc..8dbff4287816 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -330,6 +330,9 @@ type MenuItemBaseProps = { /** Should selected item be marked with checkmark */ shouldShowSelectedItemCheck?: boolean; + + /** Handles what to do when hiding the tooltip */ + onHideTooltip?: () => void; }; type MenuItemProps = (IconProps | AvatarProps | NoIcon) & MenuItemBaseProps; @@ -416,7 +419,7 @@ function MenuItem( titleWithTooltips, displayInDefaultIconColor = false, contentFit = 'cover', - isPaneMenu = false, + isPaneMenu = true, shouldPutLeftPaddingWhenNoIcon = false, onFocus, onBlur, @@ -428,6 +431,7 @@ function MenuItem( tooltipShiftVertical = 0, renderTooltipContent, shouldShowSelectedItemCheck = false, + onHideTooltip, }: MenuItemProps, ref: PressableRef, ) { @@ -559,6 +563,7 @@ function MenuItem( shiftHorizontal={tooltipShiftHorizontal} shiftVertical={tooltipShiftVertical} shouldAutoDismiss + onHideTooltip={onHideTooltip} > diff --git a/src/components/MenuItemList.tsx b/src/components/MenuItemList.tsx index d33a17f90a5e..b2d79b6243ac 100644 --- a/src/components/MenuItemList.tsx +++ b/src/components/MenuItemList.tsx @@ -49,20 +49,9 @@ type MenuItemListProps = { /** Icon Height */ iconHeight?: number; - - /** Is this in the Pane */ - isPaneMenu?: boolean; }; -function MenuItemList({ - menuItems = [], - shouldUseSingleExecution = false, - wrapperStyle = {}, - icon = undefined, - iconWidth = undefined, - iconHeight = undefined, - isPaneMenu = false, -}: MenuItemListProps) { +function MenuItemList({menuItems = [], shouldUseSingleExecution = false, wrapperStyle = {}, icon = undefined, iconWidth = undefined, iconHeight = undefined}: MenuItemListProps) { const popoverAnchor = useRef(null); const {isExecuting, singleExecution} = useSingleExecution(); @@ -99,7 +88,6 @@ function MenuItemList({ icon={icon} iconWidth={iconWidth} iconHeight={iconHeight} - isPaneMenu={isPaneMenu} // eslint-disable-next-line react/jsx-props-no-spreading {...menuItemProps} disabled={!!menuItemProps.disabled || isExecuting} diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index f60b877a5d23..997106f3e649 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -6,7 +6,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import CONST from '@src/CONST'; -import type {ParentNavigationSummaryParams} from '@src/languages/types'; +import type {ParentNavigationSummaryParams} from '@src/languages/params'; import ROUTES from '@src/ROUTES'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import Text from './Text'; diff --git a/src/components/PromotedActionsBar.tsx b/src/components/PromotedActionsBar.tsx index ee940fe2cf1c..4b5977fde19b 100644 --- a/src/components/PromotedActionsBar.tsx +++ b/src/components/PromotedActionsBar.tsx @@ -37,6 +37,7 @@ type PromotedActionsType = Record P reportID?: string; isDelegateAccessRestricted: boolean; setIsNoDelegateAccessMenuVisible: (isVisible: boolean) => void; + currentSearchHash?: number; }) => PromotedAction; }; @@ -78,7 +79,7 @@ const PromotedActions = { } }, }), - hold: ({isTextHold, reportAction, reportID, isDelegateAccessRestricted, setIsNoDelegateAccessMenuVisible}) => ({ + hold: ({isTextHold, reportAction, reportID, isDelegateAccessRestricted, setIsNoDelegateAccessMenuVisible, currentSearchHash}) => ({ key: CONST.PROMOTED_ACTIONS.HOLD, icon: Expensicons.Stopwatch, text: Localize.translateLocal(`iou.${isTextHold ? 'hold' : 'unhold'}`), @@ -99,7 +100,7 @@ const PromotedActions = { return; } - ReportUtils.changeMoneyRequestHoldStatus(reportAction, ROUTES.SEARCH_REPORT.getRoute(targetedReportID)); + ReportUtils.changeMoneyRequestHoldStatus(reportAction, ROUTES.SEARCH_REPORT.getRoute(targetedReportID), currentSearchHash); }, }), } satisfies PromotedActionsType; diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx index bb704def1836..29439911e221 100644 --- a/src/components/ReceiptAudit.tsx +++ b/src/components/ReceiptAudit.tsx @@ -22,7 +22,7 @@ function ReceiptAudit({notes, shouldShowAuditResult}: ReceiptAuditProps) { let auditText = ''; if (notes.length > 0 && shouldShowAuditResult) { - auditText = translate('iou.receiptIssuesFound', notes.length); + auditText = translate('iou.receiptIssuesFound', {count: notes.length}); } else if (!notes.length && shouldShowAuditResult) { auditText = translate('common.verified'); } diff --git a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx index aff868a74bc5..2f01bb0f9f46 100644 --- a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx +++ b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx @@ -59,7 +59,7 @@ function ExportWithDropdownMenu({ const options = [ { value: CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION, - text: translate('workspace.common.exportIntegrationSelected', connectionName), + text: translate('workspace.common.exportIntegrationSelected', {connectionName}), ...optionTemplate, }, { @@ -126,7 +126,7 @@ function ExportWithDropdownMenu({ title={translate('workspace.exportAgainModal.title')} onConfirm={confirmExport} onCancel={() => setModalStatus(null)} - prompt={translate('workspace.exportAgainModal.description', report?.reportName ?? '', connectionName)} + prompt={translate('workspace.exportAgainModal.description', {connectionName, reportName: report?.reportName ?? ''})} confirmText={translate('workspace.exportAgainModal.confirmText')} cancelText={translate('workspace.exportAgainModal.cancelText')} isVisible={!!modalStatus} diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index 6324ebeb627d..032be0a00b14 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -17,6 +17,7 @@ import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalD import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -63,7 +64,9 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); + const theme = useTheme(); const [taskReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`); + // The reportAction might not contain details regarding the taskReport // Only the direct parent reportAction will contain details about the taskReport // Other linked reportActions will only contain the taskReportID and we will grab the details from there @@ -79,6 +82,7 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che const isDeletedParentAction = ReportUtils.isCanceledTaskReport(taskReport, action); const iconWrapperStyle = StyleUtils.getTaskPreviewIconWrapper(hasAssignee ? avatarSize : undefined); + const shouldShowGreenDotIndicator = ReportUtils.isOpenTaskReport(taskReport, action) && ReportUtils.isReportManager(taskReport); if (isDeletedParentAction) { return ${translate('parentReportAction.deletedTask')}`} />; } @@ -122,6 +126,14 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che )} {taskTitle} + {shouldShowGreenDotIndicator && ( + + + + )} ; -}; - function ThreeDotsMenu({ iconTooltip = 'common.more', icon = Expensicons.ThreeDots, @@ -36,8 +29,9 @@ function ThreeDotsMenu({ shouldOverlay = false, shouldSetModalVisibility = true, disabled = false, - modal = {}, }: ThreeDotsMenuProps) { + const [modal] = useOnyx(ONYXKEYS.MODAL); + const theme = useTheme(); const styles = useThemeStyles(); const [isPopupMenuVisible, setPopupMenuVisible] = useState(false); @@ -114,8 +108,4 @@ function ThreeDotsMenu({ ThreeDotsMenu.displayName = 'ThreeDotsMenu'; -export default withOnyx({ - modal: { - key: ONYXKEYS.MODAL, - }, -})(ThreeDotsMenu); +export default ThreeDotsMenu; diff --git a/src/components/ThreeDotsMenu/types.ts b/src/components/ThreeDotsMenu/types.ts index 6c3618ffc3ce..86a10d08d449 100644 --- a/src/components/ThreeDotsMenu/types.ts +++ b/src/components/ThreeDotsMenu/types.ts @@ -1,18 +1,11 @@ import type {StyleProp, ViewStyle} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import type {TranslationPaths} from '@src/languages/types'; import type {AnchorPosition} from '@src/styles'; -import type {Modal} from '@src/types/onyx'; import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; import type IconAsset from '@src/types/utils/IconAsset'; -type ThreeDotsMenuOnyxProps = { - /** Details about any modals being used */ - modal: OnyxEntry; -}; - -type ThreeDotsMenuProps = ThreeDotsMenuOnyxProps & { +type ThreeDotsMenuProps = { /** Tooltip for the popup icon */ iconTooltip?: TranslationPaths; diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index e42f95874b42..f586c20cba49 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -34,7 +34,7 @@ function BaseGenericTooltip({ }, wrapperStyle = {}, shouldUseOverlay = false, - onPressOverlay = () => {}, + onHideTooltip = () => {}, }: BaseGenericTooltipProps) { // The width of tooltip's inner content. Has to be undefined in the beginning // as a width of 0 will cause the content to be rendered of a width of 0, @@ -102,7 +102,7 @@ function BaseGenericTooltip({ return ( - {shouldUseOverlay && } + {shouldUseOverlay && } {}, + onHideTooltip = () => {}, }: BaseGenericTooltipProps) { // The width of tooltip's inner content. Has to be undefined in the beginning // as a width of 0 will cause the content to be rendered of a width of 0, @@ -119,7 +119,7 @@ function BaseGenericTooltip({ return ReactDOM.createPortal( <> - {shouldUseOverlay && } + {shouldUseOverlay && } void; } & Pick< SharedTooltipProps, - 'renderTooltipContent' | 'maxWidth' | 'numberOfLines' | 'text' | 'shouldForceRenderingBelow' | 'wrapperStyle' | 'anchorAlignment' | 'shouldUseOverlay' | 'onPressOverlay' + 'renderTooltipContent' | 'maxWidth' | 'numberOfLines' | 'text' | 'shouldForceRenderingBelow' | 'wrapperStyle' | 'anchorAlignment' | 'shouldUseOverlay' | 'onHideTooltip' >; // eslint-disable-next-line import/prefer-default-export diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index d0ff254324ae..ef5327feba31 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -1,7 +1,9 @@ import React, {memo, useEffect, useRef, useState} from 'react'; import type {LayoutRectangle, NativeSyntheticEvent} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import GenericTooltip from '@components/Tooltip/GenericTooltip'; import type {EducationalTooltipProps} from '@components/Tooltip/types'; +import ONYXKEYS from '@src/ONYXKEYS'; import measureTooltipCoordinate from './measureTooltipCoordinate'; type LayoutChangeEventWithTarget = NativeSyntheticEvent<{layout: LayoutRectangle; target: HTMLElement}>; @@ -10,11 +12,14 @@ type LayoutChangeEventWithTarget = NativeSyntheticEvent<{layout: LayoutRectangle * A component used to wrap an element intended for displaying a tooltip. * This tooltip would show immediately without user's interaction and hide after 5 seconds. */ -function BaseEducationalTooltip({children, shouldAutoDismiss = false, shouldRender = false, ...props}: EducationalTooltipProps) { +function BaseEducationalTooltip({children, onHideTooltip, shouldAutoDismiss = false, ...props}: EducationalTooltipProps) { const hideTooltipRef = useRef<() => void>(); const [shouldMeasure, setShouldMeasure] = useState(false); const show = useRef<() => void>(); + const [modal] = useOnyx(ONYXKEYS.MODAL); + + const shouldShow = !modal?.willAlertModalBecomeVisible && !modal?.isVisible; useEffect( () => () => { @@ -33,27 +38,38 @@ function BaseEducationalTooltip({children, shouldAutoDismiss = false, shouldRend return; } - const timerID = setTimeout(hideTooltipRef.current, 5000); + // If the modal is open, hide the tooltip immediately and clear the timeout + if (!shouldShow) { + hideTooltipRef.current(); + return; + } + + // Automatically hide tooltip after 5 seconds if shouldAutoDismiss is true + const timerID = setTimeout(() => { + hideTooltipRef.current?.(); + onHideTooltip?.(); + }, 5000); return () => { clearTimeout(timerID); }; - }, [shouldAutoDismiss]); + }, [shouldAutoDismiss, shouldShow, onHideTooltip]); useEffect(() => { - if (!shouldRender || !shouldMeasure) { + if (!shouldMeasure || !shouldShow) { return; } // When tooltip is used inside an animated view (e.g. popover), we need to wait for the animation to finish before measuring content. setTimeout(() => { show.current?.(); }, 500); - }, [shouldMeasure, shouldRender]); + }, [shouldMeasure, shouldShow]); return ( {({showTooltip, hideTooltip, updateTargetBounds}) => { // eslint-disable-next-line react-compiler/react-compiler diff --git a/src/components/Tooltip/EducationalTooltip/index.tsx b/src/components/Tooltip/EducationalTooltip/index.tsx index 03500f768dd9..a97e36a5904c 100644 --- a/src/components/Tooltip/EducationalTooltip/index.tsx +++ b/src/components/Tooltip/EducationalTooltip/index.tsx @@ -2,7 +2,11 @@ import React from 'react'; import type {TooltipExtendedProps} from '@components/Tooltip/types'; import BaseEducationalTooltip from './BaseEducationalTooltip'; -function EducationalTooltip({children, ...props}: TooltipExtendedProps) { +function EducationalTooltip({children, shouldRender = false, ...props}: TooltipExtendedProps) { + if (!shouldRender) { + return children; + } + return ( {}, + onHideTooltip = () => {}, }: GenericTooltipProps) { const {preferredLocale} = useLocalize(); const {windowWidth} = useWindowDimensions(); @@ -150,8 +150,8 @@ function GenericTooltip({ } setShouldUseOverlay(false); hideTooltip(); - onPressOverlayProp(); - }, [shouldUseOverlay, onPressOverlayProp, hideTooltip]); + onHideTooltip(); + }, [shouldUseOverlay, onHideTooltip, hideTooltip]); useImperativeHandle(TooltipRefManager.ref, () => ({hideTooltip}), [hideTooltip]); @@ -183,7 +183,7 @@ function GenericTooltip({ wrapperStyle={wrapperStyle} anchorAlignment={anchorAlignment} shouldUseOverlay={shouldUseOverlay} - onPressOverlay={onPressOverlay} + onHideTooltip={onPressOverlay} /> )} diff --git a/src/components/Tooltip/types.ts b/src/components/Tooltip/types.ts index 0462b36fa524..0924f5d46a28 100644 --- a/src/components/Tooltip/types.ts +++ b/src/components/Tooltip/types.ts @@ -40,8 +40,8 @@ type SharedTooltipProps = { /** Should render a fullscreen transparent overlay */ shouldUseOverlay?: boolean; - /** Callback to fire when the transparent overlay is pressed */ - onPressOverlay?: () => void; + /** Handles what to do when hiding the tooltip */ + onHideTooltip?: () => void; }; type GenericTooltipState = { diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 14943a42a9d8..84eb988d0758 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -441,7 +441,7 @@ function BaseVideoPlayer({ )} - {((isLoading && !isOffline) || isBuffering) && } + {((isLoading && !isOffline) || (isBuffering && !isPlaying)) && } {isLoading && (isOffline || !isBuffering) && } {controlStatusState !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen) && ( ; type AllCountries = Record; /* eslint-disable max-len */ -export default { +const translations = { common: { cancel: 'Cancel', dismiss: 'Dismiss', @@ -264,7 +335,7 @@ export default { fieldRequired: 'This field is required.', requestModified: 'This request is being modified by another member.', characterLimit: ({limit}: CharacterLimitParams) => `Exceeds the maximum length of ${limit} characters`, - characterLimitExceedCounter: ({length, limit}) => `Character limit exceeded (${length}/${limit})`, + characterLimitExceedCounter: ({length, limit}: CharacterLengthLimitParams) => `Character limit exceeded (${length}/${limit})`, dateInvalid: 'Please select a valid date.', invalidDateShouldBeFuture: 'Please choose today or a future date.', invalidTimeShouldBeFuture: 'Please choose a time at least one minute ahead.', @@ -643,7 +714,7 @@ export default { shouldUseYou ? `This chat is no longer active because you are no longer a member of the ${policyName} workspace.` : `This chat is no longer active because ${displayName} is no longer a member of the ${policyName} workspace.`, - [CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}: ReportArchiveReasonsPolicyDeletedParams) => + [CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}: ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams) => `This chat is no longer active because ${policyName} is no longer an active workspace.`, [CONST.REPORT.ARCHIVE_REASON.INVOICE_RECEIVER_POLICY_DELETED]: ({policyName}: ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams) => `This chat is no longer active because ${policyName} is no longer an active workspace.`, @@ -684,13 +755,13 @@ export default { dragAndDrop: 'Drag and drop your spreadsheet here, or choose a file below. Supported formats: .csv, .txt, .xls, and .xlsx.', chooseSpreadsheet: 'Select a spreadsheet file to import. Supported formats: .csv, .txt, .xls, and .xlsx.', fileContainsHeader: 'File contains column headers', - column: (name: string) => `Column ${name}`, - fieldNotMapped: (fieldName: string) => `Oops! A required field ("${fieldName}") hasn't been mapped. Please review and try again.`, - singleFieldMultipleColumns: (fieldName: string) => `Oops! You've mapped a single field ("${fieldName}") to multiple columns. Please review and try again.`, + column: ({name}: SpreadSheetColumnParams) => `Column ${name}`, + fieldNotMapped: ({fieldName}: SpreadFieldNameParams) => `Oops! A required field ("${fieldName}") hasn't been mapped. Please review and try again.`, + singleFieldMultipleColumns: ({fieldName}: SpreadFieldNameParams) => `Oops! You've mapped a single field ("${fieldName}") to multiple columns. Please review and try again.`, importSuccessfullTitle: 'Import successful', - importCategoriesSuccessfullDescription: (categories: number) => (categories > 1 ? `${categories} categories have been added.` : '1 category has been added.'), - importMembersSuccessfullDescription: (members: number) => (members > 1 ? `${members} members have been added.` : '1 member has been added.'), - importTagsSuccessfullDescription: (tags: number) => (tags > 1 ? `${tags} tags have been added.` : '1 tag has been added.'), + importCategoriesSuccessfullDescription: ({categories}: SpreadCategoriesParams) => (categories > 1 ? `${categories} categories have been added.` : '1 category has been added.'), + importMembersSuccessfullDescription: ({members}: ImportMembersSuccessfullDescriptionParams) => (members > 1 ? `${members} members have been added.` : '1 member has been added.'), + importTagsSuccessfullDescription: ({tags}: ImportTagsSuccessfullDescriptionParams) => (tags > 1 ? `${tags} tags have been added.` : '1 tag has been added.'), importFailedTitle: 'Import failed', importFailedDescription: 'Please ensure all fields are filled out correctly and try again. If the problem persists, please reach out to Concierge.', importDescription: 'Choose which fields to map from your spreadsheet by clicking the dropdown next to each imported column below.', @@ -729,7 +800,7 @@ export default { splitBill: 'Split expense', splitScan: 'Split receipt', splitDistance: 'Split distance', - paySomeone: (name: string) => `Pay ${name ?? 'someone'}`, + paySomeone: ({name}: PaySomeoneParams = {}) => `Pay ${name ?? 'someone'}`, assignTask: 'Assign task', header: 'Quick action', trackManual: 'Track expense', @@ -753,7 +824,7 @@ export default { original: 'Original', split: 'Split', splitExpense: 'Split expense', - paySomeone: ({name}: PaySomeoneParams) => `Pay ${name ?? 'someone'}`, + paySomeone: ({name}: PaySomeoneParams = {}) => `Pay ${name ?? 'someone'}`, expense: 'Expense', categorize: 'Categorize', share: 'Share', @@ -775,7 +846,7 @@ export default { receiptScanning: 'Receipt scanning...', receiptScanInProgress: 'Receipt scan in progress', receiptScanInProgressDescription: 'Receipt scan in progress. Check back later or enter the details now.', - receiptIssuesFound: (count: number) => `${count === 1 ? 'Issue' : 'Issues'} found`, + receiptIssuesFound: ({count}: DistanceRateOperationsParams) => `${count === 1 ? 'Issue' : 'Issues'} found`, fieldPending: 'Pending...', defaultRate: 'Default rate', receiptMissingDetails: 'Receipt missing details', @@ -819,15 +890,17 @@ export default { sendInvoice: ({amount}: RequestAmountParams) => `Send ${amount} invoice`, submitAmount: ({amount}: RequestAmountParams) => `submit ${amount}`, submittedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `submitted ${formattedAmount}${comment ? ` for ${comment}` : ''}`, + automaticallySubmittedAmount: ({formattedAmount}: RequestedAmountMessageParams) => + `automatically submitted ${formattedAmount} via delayed submission`, trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `tracking ${formattedAmount}${comment ? ` for ${comment}` : ''}`, splitAmount: ({amount}: SplitAmountParams) => `split ${amount}`, didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `split ${formattedAmount}${comment ? ` for ${comment}` : ''}`, yourSplit: ({amount}: UserSplitParams) => `Your split ${amount}`, payerOwesAmount: ({payer, amount, comment}: PayerOwesAmountParams) => `${payer} owes ${amount}${comment ? ` for ${comment}` : ''}`, payerOwes: ({payer}: PayerOwesParams) => `${payer} owes: `, - payerPaidAmount: ({payer, amount}: PayerPaidAmountParams): string => `${payer ? `${payer} ` : ''}paid ${amount}`, + payerPaidAmount: ({payer, amount}: PayerPaidAmountParams) => `${payer ? `${payer} ` : ''}paid ${amount}`, payerPaid: ({payer}: PayerPaidParams) => `${payer} paid: `, - payerSpentAmount: ({payer, amount}: PayerPaidAmountParams): string => `${payer} spent ${amount}`, + payerSpentAmount: ({payer, amount}: PayerPaidAmountParams) => `${payer} spent ${amount}`, payerSpent: ({payer}: PayerPaidParams) => `${payer} spent: `, managerApproved: ({manager}: ManagerApprovedParams) => `${manager} approved:`, managerApprovedAmount: ({manager, amount}: ManagerApprovedAmountParams) => `${manager} approved ${amount}`, @@ -926,7 +999,7 @@ export default { unapprove: 'Unapprove', unapproveReport: 'Unapprove report', headsUp: 'Heads up!', - unapproveWithIntegrationWarning: (accountingIntegration: string) => + unapproveWithIntegrationWarning: ({accountingIntegration}: UnapproveWithIntegrationWarningParams) => `This report has already been exported to ${accountingIntegration}. Changes to this report in Expensify may lead to data discrepancies and Expensify Card reconciliation issues. Are you sure you want to unapprove this report?`, reimbursable: 'reimbursable', nonReimbursable: 'non-reimbursable', @@ -1310,15 +1383,16 @@ export default { availableSpend: 'Remaining limit', smartLimit: { name: 'Smart limit', - title: (formattedLimit: string) => `You can spend up to ${formattedLimit} on this card, and the limit will reset as your submitted expenses are approved.`, + title: ({formattedLimit}: ViolationsOverLimitParams) => `You can spend up to ${formattedLimit} on this card, and the limit will reset as your submitted expenses are approved.`, }, fixedLimit: { name: 'Fixed limit', - title: (formattedLimit: string) => `You can spend up to ${formattedLimit} on this card, and then it will deactivate.`, + title: ({formattedLimit}: ViolationsOverLimitParams) => `You can spend up to ${formattedLimit} on this card, and then it will deactivate.`, }, monthlyLimit: { name: 'Monthly limit', - title: (formattedLimit: string) => `You can spend up to ${formattedLimit} on this card per month. The limit will reset on the 1st day of each calendar month.`, + title: ({formattedLimit}: ViolationsOverLimitParams) => + `You can spend up to ${formattedLimit} on this card per month. The limit will reset on the 1st day of each calendar month.`, }, virtualCardNumber: 'Virtual card number', physicalCardNumber: 'Physical card number', @@ -1519,7 +1593,7 @@ export default { }, }, reportDetailsPage: { - inWorkspace: ({policyName}) => `in ${policyName}`, + inWorkspace: ({policyName}: ReportPolicyNameParams) => `in ${policyName}`, }, reportDescriptionPage: { roomDescription: 'Room description', @@ -1532,7 +1606,7 @@ export default { groupChat: { lastMemberTitle: 'Heads up!', lastMemberWarning: "Since you're the last person here, leaving will make this chat inaccessible to all users. Are you sure you want to leave?", - defaultReportName: ({displayName}: {displayName: string}) => `${displayName}'s group chat`, + defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `${displayName}'s group chat`, }, languagePage: { language: 'Language', @@ -1664,7 +1738,7 @@ export default { dateShouldBeBefore: ({dateString}: DateShouldBeBeforeParams) => `Date should be before ${dateString}.`, dateShouldBeAfter: ({dateString}: DateShouldBeAfterParams) => `Date should be after ${dateString}.`, hasInvalidCharacter: 'Name can only include Latin characters.', - incorrectZipFormat: (zipFormat?: string) => `Incorrect zip code format.${zipFormat ? ` Acceptable format: ${zipFormat}` : ''}`, + incorrectZipFormat: ({zipFormat}: IncorrectZipFormatParams = {}) => `Incorrect zip code format.${zipFormat ? ` Acceptable format: ${zipFormat}` : ''}`, }, }, resendValidationForm: { @@ -1681,8 +1755,8 @@ export default { succesfullyUnlinkedLogin: 'Secondary login successfully unlinked!', }, emailDeliveryFailurePage: { - ourEmailProvider: (user: OurEmailProviderParams) => - `Our email provider has temporarily suspended emails to ${user.login} due to delivery issues. To unblock your login, please follow these steps:`, + ourEmailProvider: ({login}: OurEmailProviderParams) => + `Our email provider has temporarily suspended emails to ${login} due to delivery issues. To unblock your login, please follow these steps:`, confirmThat: ({login}: ConfirmThatParams) => `Confirm that ${login} is spelled correctly and is a real, deliverable email address. `, emailAliases: 'Email aliases such as "expenses@domain.com" must have access to their own email inbox for it to be a valid Expensify login.', ensureYourEmailClient: 'Ensure your email client allows expensify.com emails. ', @@ -2199,7 +2273,7 @@ export default { testTransactions: 'Test transactions', issueAndManageCards: 'Issue and manage cards', reconcileCards: 'Reconcile cards', - selected: ({selectedNumber}) => `${selectedNumber} selected`, + selected: ({selectedNumber}: SelectedNumberParams) => `${selectedNumber} selected`, settlementFrequency: 'Settlement frequency', deleteConfirmation: 'Are you sure you want to delete this workspace?', unavailable: 'Unavailable workspace', @@ -2218,7 +2292,7 @@ export default { `You have been invited to ${workspaceName || 'a workspace'}! Download the Expensify mobile app at use.expensify.com/download to start tracking your expenses.`, subscription: 'Subscription', markAsExported: 'Mark as manually entered', - exportIntegrationSelected: (connectionName: ConnectionName) => `Export to ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`, + exportIntegrationSelected: ({connectionName}: ExportIntegrationSelectedParams) => `Export to ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`, letsDoubleCheck: "Let's double check that everything looks right.", lineItemLevel: 'Line-item level', reportLevel: 'Report level', @@ -2235,13 +2309,13 @@ export default { createNewConnection: 'Create new connection', reuseExistingConnection: 'Reuse existing connection', existingConnections: 'Existing connections', - lastSyncDate: (connectionName: string, formattedDate: string) => `${connectionName} - Last synced ${formattedDate}`, - authenticationError: (connectionName: string) => `Can’t connect to ${connectionName} due to an authentication error.`, + lastSyncDate: ({connectionName, formattedDate}: LastSyncDateParams) => `${connectionName} - Last synced ${formattedDate}`, + authenticationError: ({connectionName}: AuthenticationErrorParams) => `Can’t connect to ${connectionName} due to an authentication error.`, learnMore: 'Learn more.', memberAlternateText: 'Members can submit and approve reports.', adminAlternateText: 'Admins have full edit access to all reports and workspace settings.', auditorAlternateText: 'Auditors can view and comment on reports.', - roleName: (role?: string): string => { + roleName: ({role}: OptionalParam = {}) => { switch (role) { case CONST.POLICY.ROLE.ADMIN: return 'Admin'; @@ -2366,8 +2440,8 @@ export default { accountsSwitchDescription: 'Enabled categories will be available for members to select when creating their expenses.', trackingCategories: 'Tracking categories', trackingCategoriesDescription: 'Choose how to handle Xero tracking categories in Expensify.', - mapTrackingCategoryTo: ({categoryName}) => `Map Xero ${categoryName} to`, - mapTrackingCategoryToDescription: ({categoryName}) => `Choose where to map ${categoryName} when exporting to Xero.`, + mapTrackingCategoryTo: ({categoryName}: CategoryNameParams) => `Map Xero ${categoryName} to`, + mapTrackingCategoryToDescription: ({categoryName}: CategoryNameParams) => `Choose where to map ${categoryName} when exporting to Xero.`, customers: 'Re-bill customers', customersDescription: 'Choose whether to re-bill customers in Expensify. Your Xero customer contacts can be tagged to expenses, and will export to Xero as a sales invoice.', taxesDescription: 'Choose how to handle Xero taxes in Expensify.', @@ -2464,7 +2538,7 @@ export default { }, creditCardAccount: 'Credit card account', defaultVendor: 'Default vendor', - defaultVendorDescription: (isReimbursable: boolean): string => + defaultVendorDescription: ({isReimbursable}: DefaultVendorDescriptionParams) => `Set a default vendor that will apply to ${isReimbursable ? '' : 'non-'}reimbursable expenses that don't have a matching vendor in Sage Intacct.`, exportDescription: 'Configure how Expensify data exports to Sage Intacct.', exportPreferredExporterNote: @@ -2678,12 +2752,12 @@ export default { importJobs: 'Import projects', customers: 'customers', jobs: 'projects', - label: (importFields: string[], importType: string) => `${importFields.join(' and ')}, ${importType}`, + label: ({importFields, importType}: CustomersOrJobsLabelParams) => `${importFields.join(' and ')}, ${importType}`, }, importTaxDescription: 'Import tax groups from NetSuite.', importCustomFields: { chooseOptionBelow: 'Choose an option below:', - requiredFieldError: (fieldName: string) => `Please enter the ${fieldName}`, + requiredFieldError: ({fieldName}: RequiredFieldParams) => `Please enter the ${fieldName}`, customSegments: { title: 'Custom segments/records', addText: 'Add custom segment/record', @@ -2724,7 +2798,7 @@ export default { customRecordMappingTitle: 'How should this custom record be displayed in Expensify?', }, errors: { - uniqueFieldError: (fieldName: string) => `A custom segment/record with this ${fieldName?.toLowerCase()} already exists.`, + uniqueFieldError: ({fieldName}: RequiredFieldParams) => `A custom segment/record with this ${fieldName?.toLowerCase()} already exists.`, }, }, customLists: { @@ -2758,18 +2832,18 @@ export default { [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: { label: 'NetSuite employee default', description: 'Not imported into Expensify, applied on export', - footerContent: (importField: string) => + footerContent: ({importField}: ImportFieldParams) => `If you use ${importField} in NetSuite, we'll apply the default set on the employee record upon export to Expense Report or Journal Entry.`, }, [CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG]: { label: 'Tags', description: 'Line-item level', - footerContent: (importField: string) => `${startCase(importField)} will be selectable for each individual expense on an employee's report.`, + footerContent: ({importField}: ImportFieldParams) => `${startCase(importField)} will be selectable for each individual expense on an employee's report.`, }, [CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: { label: 'Report fields', description: 'Report level', - footerContent: (importField: string) => `${startCase(importField)} selection will apply to all expense on an employee's report.`, + footerContent: ({importField}: ImportFieldParams) => `${startCase(importField)} selection will apply to all expense on an employee's report.`, }, }, }, @@ -2800,8 +2874,8 @@ export default { addAUserDefinedDimension: 'Add a user-defined dimension', detailedInstructionsLink: 'View detailed instructions', detailedInstructionsRestOfSentence: ' on adding user-defined dimensions.', - userDimensionsAdded: (dimensionsCount: number) => `${dimensionsCount} ${Str.pluralize('UDD', `UDDs`, dimensionsCount)} added`, - mappingTitle: (mappingName: SageIntacctMappingName): string => { + userDimensionsAdded: ({dimensionsCount}: DimensionsCountParams) => `${dimensionsCount} ${Str.pluralize('UDD', `UDDs`, dimensionsCount)} added`, + mappingTitle: ({mappingName}: IntacctMappingTitleParams) => { switch (mappingName) { case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.DEPARTMENTS: return 'departments'; @@ -2835,7 +2909,7 @@ export default { }, yourCardProvider: `Who's your card provider?`, enableFeed: { - title: (provider: string) => `Enable your ${provider} feed`, + title: ({provider}: GoBackMessageParams) => `Enable your ${provider} feed`, heading: 'We have a direct integration with your card issuer and can import your transaction data into Expensify quickly and accurately.\n\nTo get started, simply:', visa: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) for detailed instructionson how to set up your Visa Commercial Cards.\n\n2. [Contact your bank](${CONST.COMPANY_CARDS_HELP}) to verify they support a custom feed for your program, and ask them toenable it.\n\n3. *Once the feed is enabled and you have its details, continue to the next screen.*`, amex: `1. Visit [this help article](${CONST.COMPANY_CARDS_HELP}) to find out if American Express can enable a custom feed for your program.\n\n2. Once the feed is enabled, Amex will send you a production letter.\n\n3. *Once you have the feed information, continue to the next screen.*`, @@ -2882,7 +2956,7 @@ export default { card: 'Card', startTransactionDate: 'Start transaction date', cardName: 'Card name', - assignedYouCard: (assigner: string) => `${assigner} assigned you a company card! Imported transactions will appear in this chat.`, + assignedYouCard: ({assigner}: AssignedYouCardParams) => `${assigner} assigned you a company card! Imported transactions will appear in this chat.`, chooseCardFeed: 'Choose card feed', }, expensifyCard: { @@ -2928,20 +3002,21 @@ export default { deactivate: 'Deactivate card', changeCardLimit: 'Change card limit', changeLimit: 'Change limit', - smartLimitWarning: (limit: string) => `If you change this card’s limit to ${limit}, new transactions will be declined until you approve more expenses on the card.`, - monthlyLimitWarning: (limit: string) => `If you change this card’s limit to ${limit}, new transactions will be declined until next month.`, - fixedLimitWarning: (limit: string) => `If you change this card’s limit to ${limit}, new transactions will be declined.`, + smartLimitWarning: ({limit}: CharacterLimitParams) => + `If you change this card’s limit to ${limit}, new transactions will be declined until you approve more expenses on the card.`, + monthlyLimitWarning: ({limit}: CharacterLimitParams) => `If you change this card’s limit to ${limit}, new transactions will be declined until next month.`, + fixedLimitWarning: ({limit}: CharacterLimitParams) => `If you change this card’s limit to ${limit}, new transactions will be declined.`, changeCardLimitType: 'Change card limit type', changeLimitType: 'Change limit type', - changeCardSmartLimitTypeWarning: (limit: string) => + changeCardSmartLimitTypeWarning: ({limit}: CharacterLimitParams) => `If you change this card's limit type to Smart Limit, new transactions will be declined because the ${limit} unapproved limit has already been reached.`, - changeCardMonthlyLimitTypeWarning: (limit: string) => + changeCardMonthlyLimitTypeWarning: ({limit}: CharacterLimitParams) => `If you change this card's limit type to Monthly, new transactions will be declined because the ${limit} monthly limit has already been reached.`, addShippingDetails: 'Add shipping details', - issuedCard: (assignee: string) => `issued ${assignee} an Expensify Card! The card will arrive in 2-3 business days.`, - issuedCardNoShippingDetails: (assignee: string) => `issued ${assignee} an Expensify Card! The card will be shipped once shipping details are added.`, + issuedCard: ({assignee}: AssigneeParams) => `issued ${assignee} an Expensify Card! The card will arrive in 2-3 business days.`, + issuedCardNoShippingDetails: ({assignee}: AssigneeParams) => `issued ${assignee} an Expensify Card! The card will be shipped once shipping details are added.`, issuedCardVirtual: ({assignee, link}: IssueVirtualCardParams) => `issued ${assignee} a virtual ${link}! The card can be used right away.`, - addedShippingDetails: (assignee: string) => `${assignee} added shipping details. Expensify Card will arrive in 2-3 business days.`, + addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} added shipping details. Expensify Card will arrive in 2-3 business days.`, }, categories: { deleteCategories: 'Delete categories', @@ -3040,8 +3115,8 @@ export default { cardNumber: 'Card number', cardholder: 'Cardholder', cardName: 'Card name', - integrationExport: (integration: string, type: string) => `${integration} ${type} export`, - integrationExportTitleFirstPart: (integration: string) => `Choose the ${integration} account where transactions should be exported. Select a different`, + integrationExport: ({integration, type}: IntegrationExportParams) => `${integration} ${type} export`, + integrationExportTitleFirstPart: ({integration}: IntegrationExportParams) => `Choose the ${integration} account where transactions should be exported. Select a different`, integrationExportTitleLinkPart: 'export option', integrationExportTitleSecondPart: 'to change the available accounts.', lastUpdated: 'Last updated', @@ -3074,7 +3149,7 @@ export default { giveItNameInstruction: 'Give the card a name that sets it apart from the others.', updating: 'Updating...', noAccountsFound: 'No accounts found', - noAccountsFoundDescription: (connection: string) => `Please add the account in ${connection} and sync the connection again.`, + noAccountsFoundDescription: ({connection}: ConnectionParams) => `Please add the account in ${connection} and sync the connection again.`, }, workflows: { title: 'Workflows', @@ -3195,7 +3270,7 @@ export default { tagRules: 'Tag rules', approverDescription: 'Approver', importTags: 'Import tags', - importedTagsMessage: (columnCounts: number) => + importedTagsMessage: ({columnCounts}: ImportedTagsMessageParams) => `We found *${columnCounts} columns* in your spreadsheet. Select *Name* next to the column that contains tags names. You can also select *Enabled* next to the column that sets tags status.`, }, taxes: { @@ -3218,7 +3293,7 @@ export default { updateTaxClaimableFailureMessage: 'The reclaimable portion must be less than the distance rate amount.', }, deleteTaxConfirmation: 'Are you sure you want to delete this tax?', - deleteMultipleTaxConfirmation: ({taxAmount}) => `Are you sure you want to delete ${taxAmount} taxes?`, + deleteMultipleTaxConfirmation: ({taxAmount}: TaxAmountParams) => `Are you sure you want to delete ${taxAmount} taxes?`, actions: { delete: 'Delete rate', deleteMultiple: 'Delete rates', @@ -3261,7 +3336,7 @@ export default { removeWorkspaceMemberButtonTitle: 'Remove from workspace', removeGroupMemberButtonTitle: 'Remove from group', removeRoomMemberButtonTitle: 'Remove from chat', - removeMemberPrompt: ({memberName}: {memberName: string}) => `Are you sure you want to remove ${memberName}?`, + removeMemberPrompt: ({memberName}: RemoveMemberPromptParams) => `Are you sure you want to remove ${memberName}?`, removeMemberTitle: 'Remove member', transferOwner: 'Transfer owner', makeMember: 'Make member', @@ -3274,7 +3349,7 @@ export default { genericRemove: 'There was a problem removing that workspace member.', }, addedWithPrimary: 'Some members were added with their primary logins.', - invitedBySecondaryLogin: ({secondaryLogin}) => `Added by secondary login ${secondaryLogin}.`, + invitedBySecondaryLogin: ({secondaryLogin}: SecondaryLoginParams) => `Added by secondary login ${secondaryLogin}.`, membersListTitle: 'Directory of all workspace members.', importMembers: 'Import members', }, @@ -3322,8 +3397,8 @@ export default { xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', - connectionName: (integration: ConnectionName) => { - switch (integration) { + connectionName: ({connectionName}: ConnectionNameParams) => { + switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: return 'Quickbooks Online'; case CONST.POLICY.CONNECTIONS.NAME.XERO: @@ -3340,21 +3415,22 @@ export default { errorODIntegration: "There's an error with a connection that's been set up in Expensify Classic. ", goToODToFix: 'Go to Expensify Classic to fix this issue.', setup: 'Connect', - lastSync: (relativeDate: string) => `Last synced ${relativeDate}`, + lastSync: ({relativeDate}: LastSyncAccountingParams) => `Last synced ${relativeDate}`, import: 'Import', export: 'Export', advanced: 'Advanced', other: 'Other integrations', syncNow: 'Sync now', disconnect: 'Disconnect', - disconnectTitle: (integration?: ConnectionName): string => { - const integrationName = integration && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integration] ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integration] : 'integration'; + disconnectTitle: ({connectionName}: OptionalParam = {}) => { + const integrationName = + connectionName && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] : 'integration'; return `Disconnect ${integrationName}`; }, - connectTitle: (integrationToConnect: ConnectionName): string => `Connect ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integrationToConnect] ?? 'accounting integration'}`, + connectTitle: ({connectionName}: ConnectionNameParams) => `Connect ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ?? 'accounting integration'}`, - syncError: (integration?: ConnectionName): string => { - switch (integration) { + syncError: ({connectionName}: ConnectionNameParams) => { + switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: return "Can't connect to QuickBooks Online."; case CONST.POLICY.CONNECTIONS.NAME.XERO: @@ -3380,20 +3456,18 @@ export default { [CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: 'Imported as report fields', [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'NetSuite employee default', }, - disconnectPrompt: (currentIntegration?: ConnectionName): string => { + disconnectPrompt: ({connectionName}: OptionalParam = {}) => { const integrationName = - currentIntegration && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[currentIntegration] - ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[currentIntegration] - : 'this integration'; + connectionName && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] : 'this integration'; return `Are you sure you want to disconnect ${integrationName}?`; }, - connectPrompt: (integrationToConnect: ConnectionName): string => + connectPrompt: ({connectionName}: ConnectionNameParams) => `Are you sure you want to connect ${ - CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integrationToConnect] ?? 'this accounting integration' + CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ?? 'this accounting integration' }? This will remove any existing acounting connections.`, enterCredentials: 'Enter your credentials', connections: { - syncStageName: (stage: PolicyConnectionSyncStage) => { + syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'quickbooksOnlineImportCustomers': return 'Importing customers'; @@ -3530,7 +3604,7 @@ export default { chooseBankAccount: 'Choose the bank account that your Expensify Card payments will be reconciled against.', accountMatches: 'Make sure this account matches your ', settlementAccount: 'Expensify Card settlement account ', - reconciliationWorks: (lastFourPAN: string) => `(ending in ${lastFourPAN}) so Continuous Reconciliation works properly.`, + reconciliationWorks: ({lastFourPAN}: ReconciliationWorksParams) => `(ending in ${lastFourPAN}) so Continuous Reconciliation works properly.`, }, }, export: { @@ -3590,7 +3664,10 @@ export default { rate: 'Rate', addRate: 'Add rate', trackTax: 'Track tax', - deleteRates: ({count}: DistanceRateOperationsParams) => `Delete ${Str.pluralize('rate', 'rates', count)}`, + deleteRates: () => ({ + one: 'Delete rate', + other: 'Delete rates', + }), enableRates: ({count}: DistanceRateOperationsParams) => `Enable ${Str.pluralize('rate', 'rates', count)}`, disableRates: ({count}: DistanceRateOperationsParams) => `Disable ${Str.pluralize('rate', 'rates', count)}`, enableRate: 'Enable rate', @@ -3659,19 +3736,19 @@ export default { amountOwedText: 'This account has an outstanding balance from a previous month.\n\nDo you want to clear the balance and take over billing of this workspace?', ownerOwesAmountTitle: 'Outstanding balance', ownerOwesAmountButtonText: 'Transfer balance', - ownerOwesAmountText: ({email, amount}) => + ownerOwesAmountText: ({email, amount}: OwnerOwesAmountParams) => `The account owning this workspace (${email}) has an outstanding balance from a previous month.\n\nDo you want to transfer this amount (${amount}) in order to take over billing for this workspace? Your payment card will be charged immediately.`, subscriptionTitle: 'Take over annual subscription', subscriptionButtonText: 'Transfer subscription', - subscriptionText: ({usersCount, finalCount}) => + subscriptionText: ({usersCount, finalCount}: ChangeOwnerSubscriptionParams) => `Taking over this workspace will merge its annual subscription with your current subscription. This will increase your subscription size by ${usersCount} members making your new subscription size ${finalCount}. Would you like to continue?`, duplicateSubscriptionTitle: 'Duplicate subscription alert', duplicateSubscriptionButtonText: 'Continue', - duplicateSubscriptionText: ({email, workspaceName}) => + duplicateSubscriptionText: ({email, workspaceName}: ChangeOwnerDuplicateSubscriptionParams) => `It looks like you may be trying to take over billing for ${email}'s workspaces, but to do that, you need to be an admin on all their workspaces first.\n\nClick "Continue" if you only want to take over billing for the workspace ${workspaceName}.\n\nIf you want to take over billing for their entire subscription, please have them add you as an admin to all their workspaces first before taking over billing.`, hasFailedSettlementsTitle: 'Cannot transfer ownership', hasFailedSettlementsButtonText: 'Got it', - hasFailedSettlementsText: ({email}) => + hasFailedSettlementsText: ({email}: ChangeOwnerHasFailedSettlementsParams) => `You can't take over billing because ${email} has an overdue expensify Expensify Card settlement. Please ask them to reach out to concierge@expensify.com to resolve the issue. Then, you can take over billing for this workspace.`, failedToClearBalanceTitle: 'Failed to clear balance', failedToClearBalanceButtonText: 'OK', @@ -3685,7 +3762,7 @@ export default { }, exportAgainModal: { title: 'Careful!', - description: (reportName: string, connectionName: ConnectionName) => + description: ({reportName, connectionName}: ExportAgainModalDescriptionParams) => `The following reports have already been exported to ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}:\n\n${reportName}\n\nAre you sure you want to export them again?`, confirmText: 'Yes, export again', cancelText: 'Cancel', @@ -3748,7 +3825,7 @@ export default { upgradeToUnlock: 'Unlock this feature', completed: { headline: `You've upgraded your workspace!`, - successMessage: (policyName: string) => `You've successfully upgraded your ${policyName} workspace to the Control plan!`, + successMessage: ({policyName}: ReportPolicyNameParams) => `You've successfully upgraded your ${policyName} workspace to the Control plan!`, viewSubscription: 'View your subscription', moreDetails: 'for more details.', gotIt: 'Got it, thanks', @@ -3756,8 +3833,8 @@ export default { }, restrictedAction: { restricted: 'Restricted', - actionsAreCurrentlyRestricted: ({workspaceName}) => `Actions on the ${workspaceName} workspace are currently restricted`, - workspaceOwnerWillNeedToAddOrUpdatePaymentCard: ({workspaceOwnerName}) => + actionsAreCurrentlyRestricted: ({workspaceName}: ActionsAreCurrentlyRestricted) => `Actions on the ${workspaceName} workspace are currently restricted`, + workspaceOwnerWillNeedToAddOrUpdatePaymentCard: ({workspaceOwnerName}: WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams) => `Workspace owner, ${workspaceOwnerName} will need to add or update the payment card on file to unlock new workspace activity.`, youWillNeedToAddOrUpdatePaymentCard: "You'll need to add or update the payment card on file to unlock new workspace activity.", addPaymentCardToUnlock: 'Add a payment card to unlock!', @@ -3778,7 +3855,7 @@ export default { maxAge: 'Max age', maxExpenseAge: 'Max expense age', maxExpenseAgeDescription: 'Flag spend older than a specific number of days.', - maxExpenseAgeDays: (age: number) => `${age} ${Str.pluralize('day', 'days', age)}`, + maxExpenseAgeDays: ({age}: AgeParams) => `${age} ${Str.pluralize('day', 'days', age)}`, billableDefault: 'Billable default', billableDefaultDescription: 'Choose whether cash and credit card expenses should be billable by default. Billable expenses are enabled or disabled in', billable: 'Billable', @@ -3815,26 +3892,26 @@ export default { randomReportAuditDescription: 'Require that some reports be manually approved, even if eligible for auto-approval.', autoPayApprovedReportsTitle: 'Auto-pay approved reports', autoPayApprovedReportsSubtitle: 'Configure which expense reports are eligible for auto-pay.', - autoPayApprovedReportsLimitError: (currency?: string) => `Please enter an amount less than ${currency ?? ''}20,000`, + autoPayApprovedReportsLimitError: ({currency}: AutoPayApprovedReportsLimitErrorParams = {}) => `Please enter an amount less than ${currency ?? ''}20,000`, autoPayApprovedReportsLockedSubtitle: 'Go to more features and enable workflows, then add payments to unlock this feature.', autoPayReportsUnderTitle: 'Auto-pay reports under', autoPayReportsUnderDescription: 'Fully compliant expense reports under this amount will be automatically paid. ', unlockFeatureGoToSubtitle: 'Go to', - unlockFeatureEnableWorkflowsSubtitle: (featureName: string) => `and enable workflows, then add ${featureName} to unlock this feature.`, - enableFeatureSubtitle: (featureName: string) => `and enable ${featureName} to unlock this feature.`, + unlockFeatureEnableWorkflowsSubtitle: ({featureName}: FeatureNameParams) => `and enable workflows, then add ${featureName} to unlock this feature.`, + enableFeatureSubtitle: ({featureName}: FeatureNameParams) => `and enable ${featureName} to unlock this feature.`, }, categoryRules: { title: 'Category rules', approver: 'Approver', requireDescription: 'Require description', descriptionHint: 'Description hint', - descriptionHintDescription: (categoryName: string) => + descriptionHintDescription: ({categoryName}: CategoryNameParams) => `Remind employees to provide additional information for “${categoryName}” spend. This hint appears in the description field on expenses.`, descriptionHintLabel: 'Hint', descriptionHintSubtitle: 'Pro-tip: The shorter the better!', maxAmount: 'Max amount', flagAmountsOver: 'Flag amounts over', - flagAmountsOverDescription: (categoryName) => `Applies to the category “${categoryName}”.`, + flagAmountsOverDescription: ({categoryName}: CategoryNameParams) => `Applies to the category “${categoryName}”.`, flagAmountsOverSubtitle: 'This overrides the max amount for all expenses.', expenseLimitTypes: { expense: 'Individual expense', @@ -3844,7 +3921,7 @@ export default { }, requireReceiptsOver: 'Require receipts over', requireReceiptsOverList: { - default: (defaultAmount: string) => `${defaultAmount} ${CONST.DOT_SEPARATOR} Default`, + default: ({defaultAmount}: DefaultAmountParams) => `${defaultAmount} ${CONST.DOT_SEPARATOR} Default`, never: 'Never require receipts', always: 'Always require receipts', }, @@ -3907,8 +3984,8 @@ export default { }, }, workspaceActions: { - renamedWorkspaceNameAction: ({oldName, newName}) => `updated the name of this workspace from ${oldName} to ${newName}`, - removedFromApprovalWorkflow: ({submittersNames}: {submittersNames: string[]}) => { + renamedWorkspaceNameAction: ({oldName, newName}: RenamedRoomActionParams) => `updated the name of this workspace from ${oldName} to ${newName}`, + removedFromApprovalWorkflow: ({submittersNames}: RemovedFromApprovalWorkflowParams) => { let joinedNames = ''; if (submittersNames.length === 1) { joinedNames = submittersNames[0]; @@ -3961,7 +4038,7 @@ export default { deleteConfirmation: 'Are you sure you want to delete this task?', }, statementPage: { - title: (year, monthName) => `${monthName} ${year} statement`, + title: ({year, monthName}: StatementTitleParams) => `${monthName} ${year} statement`, generatingPDF: "We're generating your PDF right now. Please check back soon!", }, keyboardShortcutsPage: { @@ -4011,8 +4088,8 @@ export default { filtersHeader: 'Filters', filters: { date: { - before: (date?: string) => `Before ${date ?? ''}`, - after: (date?: string) => `After ${date ?? ''}`, + before: ({date}: OptionalParam = {}) => `Before ${date ?? ''}`, + after: ({date}: OptionalParam = {}) => `After ${date ?? ''}`, }, status: 'Status', keyword: 'Keyword', @@ -4022,9 +4099,9 @@ export default { pinned: 'Pinned', unread: 'Unread', amount: { - lessThan: (amount?: string) => `Less than ${amount ?? ''}`, - greaterThan: (amount?: string) => `Greater than ${amount ?? ''}`, - between: (greaterThan: string, lessThan: string) => `Between ${greaterThan} and ${lessThan}`, + lessThan: ({amount}: OptionalParam = {}) => `Less than ${amount ?? ''}`, + greaterThan: ({amount}: OptionalParam = {}) => `Greater than ${amount ?? ''}`, + between: ({greaterThan, lessThan}: FiltersAmountBetweenParams) => `Between ${greaterThan} and ${lessThan}`, }, current: 'Current', past: 'Past', @@ -4144,7 +4221,7 @@ export default { nonReimbursableLink: 'View company card expenses.', pending: ({label}: ExportedToIntegrationParams) => `started exporting this report to ${label}...`, }, - integrationsMessage: (errorMessage: string, label: string) => `failed to export this report to ${label} ("${errorMessage}").`, + integrationsMessage: ({errorMessage, label}: IntegrationSyncFailedParams) => `failed to export this report to ${label} ("${errorMessage}").`, managerAttachReceipt: `added a receipt`, managerDetachReceipt: `removed a receipt`, markedReimbursed: ({amount, currency}: MarkedReimbursedParams) => `paid ${currency}${amount} elsewhere`, @@ -4161,10 +4238,10 @@ export default { stripePaid: ({amount, currency}: StripePaidParams) => `paid ${currency}${amount}`, takeControl: `took control`, unapproved: ({amount, currency}: UnapprovedParams) => `unapproved ${currency}${amount}`, - integrationSyncFailed: (label: string, errorMessage: string) => `failed to sync with ${label} ("${errorMessage}")`, - addEmployee: (email: string, role: string) => `added ${email} as ${role === 'user' ? 'member' : 'admin'}`, - updateRole: (email: string, currentRole: string, newRole: string) => `updated the role of ${email} from ${currentRole} to ${newRole}`, - removeMember: (email: string, role: string) => `removed ${role} ${email}`, + integrationSyncFailed: ({label, errorMessage}: IntegrationSyncFailedParams) => `failed to sync with ${label} ("${errorMessage}")`, + addEmployee: ({email, role}: AddEmployeeParams) => `added ${email} as ${role === 'user' ? 'member' : 'admin'}`, + updateRole: ({email, currentRole, newRole}: UpdateRoleParams) => `updated the role of ${email} from ${currentRole} to ${newRole}`, + removeMember: ({email, role}: AddEmployeeParams) => `removed ${role} ${email}`, }, }, }, @@ -4385,7 +4462,7 @@ export default { allTagLevelsRequired: 'All tags required', autoReportedRejectedExpense: ({rejectReason, rejectedBy}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rejected this expense with the comment "${rejectReason}"`, billableExpense: 'Billable no longer valid', - cashExpenseWithNoReceipt: ({formattedLimit}: ViolationsCashExpenseWithNoReceiptParams) => `Receipt required${formattedLimit ? ` over ${formattedLimit}` : ''}`, + cashExpenseWithNoReceipt: ({formattedLimit}: ViolationsCashExpenseWithNoReceiptParams = {}) => `Receipt required${formattedLimit ? ` over ${formattedLimit}` : ''}`, categoryOutOfPolicy: 'Category no longer valid', conversionSurcharge: ({surcharge}: ViolationsConversionSurchargeParams) => `Applied ${surcharge}% conversion surcharge`, customUnitOutOfPolicy: 'Rate not valid for this workspace', @@ -4396,8 +4473,8 @@ export default { maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Date older than ${maxAge} days`, missingCategory: 'Missing category', missingComment: 'Description required for selected category', - missingTag: ({tagName}: ViolationsMissingTagParams) => `Missing ${tagName ?? 'tag'}`, - modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams): string => { + missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Missing ${tagName ?? 'tag'}`, + modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => { switch (type) { case 'distance': return 'Amount differs from calculated distance'; @@ -4445,10 +4522,10 @@ export default { return ''; }, smartscanFailed: 'Receipt scanning failed. Enter details manually.', - someTagLevelsRequired: ({tagName}: ViolationsTagOutOfPolicyParams) => `Missing ${tagName ?? 'Tag'}`, - tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `${tagName ?? 'Tag'} no longer valid`, + someTagLevelsRequired: ({tagName}: ViolationsTagOutOfPolicyParams = {}) => `Missing ${tagName ?? 'Tag'}`, + tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams = {}) => `${tagName ?? 'Tag'} no longer valid`, taxAmountChanged: 'Tax amount was modified', - taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName ?? 'Tax'} no longer valid`, + taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams = {}) => `${taxName ?? 'Tax'} no longer valid`, taxRateChanged: 'Tax rate was modified', taxRequired: 'Missing tax rate', none: 'None', @@ -4465,7 +4542,7 @@ export default { hold: 'Hold', }, reportViolations: { - [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} is required`, + [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} is required`, }, violationDismissal: { rter: { @@ -4520,12 +4597,12 @@ export default { authenticatePaymentCard: 'Authenticate payment card', mobileReducedFunctionalityMessage: 'You can’t make changes to your subscription in the mobile app.', badge: { - freeTrial: ({numOfDays}) => `Free trial: ${numOfDays} ${numOfDays === 1 ? 'day' : 'days'} left`, + freeTrial: ({numOfDays}: BadgeFreeTrialParams) => `Free trial: ${numOfDays} ${numOfDays === 1 ? 'day' : 'days'} left`, }, billingBanner: { policyOwnerAmountOwed: { title: 'Your payment info is outdated', - subtitle: ({date}) => `Update your payment card by ${date} to continue using all of your favorite features.`, + subtitle: ({date}: BillingBannerSubtitleWithDateParams) => `Update your payment card by ${date} to continue using all of your favorite features.`, }, policyOwnerAmountOwedOverdue: { title: 'Your payment info is outdated', @@ -4533,7 +4610,7 @@ export default { }, policyOwnerUnderInvoicing: { title: 'Your payment info is outdated', - subtitle: ({date}) => `Your payment is past due. Please pay your invoice by ${date} to avoid service interruption.`, + subtitle: ({date}: BillingBannerSubtitleWithDateParams) => `Your payment is past due. Please pay your invoice by ${date} to avoid service interruption.`, }, policyOwnerUnderInvoicingOverdue: { title: 'Your payment info is outdated', @@ -4541,22 +4618,22 @@ export default { }, billingDisputePending: { title: 'Your card couldn’t be charged', - subtitle: ({amountOwed, cardEnding}) => + subtitle: ({amountOwed, cardEnding}: BillingBannerDisputePendingParams) => `You disputed the ${amountOwed} charge on the card ending in ${cardEnding}. Your account will be locked until the dispute is resolved with your bank.`, }, cardAuthenticationRequired: { title: 'Your card couldn’t be charged', - subtitle: ({cardEnding}) => + subtitle: ({cardEnding}: BillingBannerCardAuthenticationRequiredParams) => `Your payment card hasn’t been fully authenticated. Please complete the authentication process to activate your payment card ending in ${cardEnding}.`, }, insufficientFunds: { title: 'Your card couldn’t be charged', - subtitle: ({amountOwed}) => + subtitle: ({amountOwed}: BillingBannerInsufficientFundsParams) => `Your payment card was declined due to insufficient funds. Please retry or add a new payment card to clear your ${amountOwed} outstanding balance.`, }, cardExpired: { title: 'Your card couldn’t be charged', - subtitle: ({amountOwed}) => `Your payment card expired. Please add a new payment card to clear your ${amountOwed} outstanding balance.`, + subtitle: ({amountOwed}: BillingBannerCardExpiredParams) => `Your payment card expired. Please add a new payment card to clear your ${amountOwed} outstanding balance.`, }, cardExpireSoon: { title: 'Your card is expiring soon', @@ -4570,7 +4647,7 @@ export default { title: 'Your card couldn’t be charged', subtitle: 'Before retrying, please call your bank directly to authorize Expensify charges and remove any holds. Otherwise, try adding a different payment card.', }, - cardOnDispute: ({amountOwed, cardEnding}) => + cardOnDispute: ({amountOwed, cardEnding}: BillingBannerCardOnDisputeParams) => `You disputed the ${amountOwed} charge on the card ending in ${cardEnding}. Your account will be locked until the dispute is resolved with your bank.`, preTrial: { title: 'Start a free trial', @@ -4579,7 +4656,7 @@ export default { subtitleEnd: 'so your team can start expensing.', }, trialStarted: { - title: ({numOfDays}) => `Free trial: ${numOfDays} ${numOfDays === 1 ? 'day' : 'days'} left!`, + title: ({numOfDays}: TrialStartedTitleParams) => `Free trial: ${numOfDays} ${numOfDays === 1 ? 'day' : 'days'} left!`, subtitle: 'Add a payment card to continue using all of your favorite features.', }, trialEnded: { @@ -4591,9 +4668,9 @@ export default { title: 'Payment', subtitle: 'Add a card to pay for your Expensify subscription.', addCardButton: 'Add payment card', - cardNextPayment: ({nextPaymentDate}) => `Your next payment date is ${nextPaymentDate}.`, - cardEnding: ({cardNumber}) => `Card ending in ${cardNumber}`, - cardInfo: ({name, expiration, currency}) => `Name: ${name}, Expiration: ${expiration}, Currency: ${currency}`, + cardNextPayment: ({nextPaymentDate}: CardNextPaymentParams) => `Your next payment date is ${nextPaymentDate}.`, + cardEnding: ({cardNumber}: CardEndingParams) => `Card ending in ${cardNumber}`, + cardInfo: ({name, expiration, currency}: CardInfoParams) => `Name: ${name}, Expiration: ${expiration}, Currency: ${currency}`, changeCard: 'Change payment card', changeCurrency: 'Change payment currency', cardNotFound: 'No payment card added', @@ -4612,8 +4689,8 @@ export default { title: 'Your plan', collect: { title: 'Collect', - priceAnnual: ({lower, upper}) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`, - pricePayPerUse: ({lower, upper}) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`, + priceAnnual: ({lower, upper}: YourPlanPriceParams) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`, + pricePayPerUse: ({lower, upper}: YourPlanPriceParams) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`, benefit1: 'Unlimited SmartScans and distance tracking', benefit2: 'Expensify Cards with Smart Limits', benefit3: 'Bill pay and invoicing', @@ -4624,8 +4701,8 @@ export default { }, control: { title: 'Control', - priceAnnual: ({lower, upper}) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`, - pricePayPerUse: ({lower, upper}) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`, + priceAnnual: ({lower, upper}: YourPlanPriceParams) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`, + pricePayPerUse: ({lower, upper}: YourPlanPriceParams) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`, benefit1: 'Everything in Collect, plus:', benefit2: 'NetSuite and Sage Intacct integrations', benefit3: 'Certinia and Workday sync', @@ -4656,10 +4733,10 @@ export default { note: 'Note: An active member is anyone who has created, edited, submitted, approved, reimbursed, or exported expense data tied to your company workspace.', confirmDetails: 'Confirm your new annual subscription details:', subscriptionSize: 'Subscription size', - activeMembers: ({size}) => `${size} active members/month`, + activeMembers: ({size}: SubscriptionSizeParams) => `${size} active members/month`, subscriptionRenews: 'Subscription renews', youCantDowngrade: 'You can’t downgrade during your annual subscription.', - youAlreadyCommitted: ({size, date}) => + youAlreadyCommitted: ({size, date}: SubscriptionCommitmentParams) => `You already committed to an annual subscription size of ${size} active members per month until ${date}. You can switch to a pay-per-use subscription on ${date} by disabling auto-renew.`, error: { size: 'Please enter a valid subscription size.', @@ -4676,13 +4753,13 @@ export default { title: 'Subscription settings', autoRenew: 'Auto-renew', autoIncrease: 'Auto-increase annual seats', - saveUpTo: ({amountWithCurrency}) => `Save up to ${amountWithCurrency}/month per active member`, + saveUpTo: ({amountWithCurrency}: SubscriptionSettingsSaveUpToParams) => `Save up to ${amountWithCurrency}/month per active member`, automaticallyIncrease: 'Automatically increase your annual seats to accommodate for active members that exceed your subscription size. Note: This will extend your annual subscription end date.', disableAutoRenew: 'Disable auto-renew', helpUsImprove: 'Help us improve Expensify', whatsMainReason: "What's the main reason you're disabling auto-renew?", - renewsOn: ({date}) => `Renews on ${date}.`, + renewsOn: ({date}: SubscriptionSettingsRenewsOnParams) => `Renews on ${date}.`, }, requestEarlyCancellation: { title: 'Request early cancellation', @@ -4731,7 +4808,7 @@ export default { addCopilot: 'Add copilot', membersCanAccessYourAccount: 'These members can access your account:', youCanAccessTheseAccounts: 'You can access these accounts via the account switcher:', - role: (role?: string): string => { + role: ({role}: OptionalParam = {}) => { switch (role) { case CONST.DELEGATE_ROLE.ALL: return 'Full'; @@ -4742,10 +4819,11 @@ export default { } }, genericError: 'Oops, something went wrong. Please try again.', + onBehalfOfMessage: ({delegator}: DelegatorParams) => `on behalf of ${delegator}`, accessLevel: 'Access level', confirmCopilot: 'Confirm your copilot below.', accessLevelDescription: 'Choose an access level below. Both Full and Limited access allow copilots to view all conversations and expenses.', - roleDescription: (role?: string): string => { + roleDescription: ({role}: OptionalParam = {}) => { switch (role) { case CONST.DELEGATE_ROLE.ALL: return 'Allow another member to take all actions in your account, on your behalf. Includes chat, submissions, approvals, payments, settings updates, and more.'; @@ -4771,9 +4849,9 @@ export default { nothingToPreview: 'Nothing to preview', editJson: 'Edit JSON:', preview: 'Preview:', - missingProperty: ({propertyName}) => `Missing ${propertyName}`, - invalidProperty: ({propertyName, expectedType}) => `Invalid property: ${propertyName} - Expected: ${expectedType}`, - invalidValue: ({expectedValues}) => `Invalid value - Expected: ${expectedValues}`, + missingProperty: ({propertyName}: MissingPropertyParams) => `Missing ${propertyName}`, + invalidProperty: ({propertyName, expectedType}: InvalidPropertyParams) => `Invalid property: ${propertyName} - Expected: ${expectedType}`, + invalidValue: ({expectedValues}: InvalidValueParams) => `Invalid value - Expected: ${expectedValues}`, missingValue: 'Missing value', createReportAction: 'Create Report Action', reportAction: 'Report Action', @@ -4788,4 +4866,6 @@ export default { time: 'Time', none: 'None', }, -} satisfies TranslationBase; +}; + +export default translations satisfies TranslationDeepObject; diff --git a/src/languages/es.ts b/src/languages/es.ts index 4e63f99357e7..8d8b33ca57dd 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1,45 +1,91 @@ import {Str} from 'expensify-common'; import CONST from '@src/CONST'; -import type {ConnectionName, PolicyConnectionSyncStage, SageIntacctMappingName} from '@src/types/onyx/Policy'; +import type en from './en'; import type { AccountOwnerParams, + ActionsAreCurrentlyRestricted, + AddEmployeeParams, AddressLineParams, AdminCanceledRequestParams, + AgeParams, AlreadySignedInParams, ApprovalWorkflowErrorParams, ApprovedAmountParams, AssignCardParams, + AssignedYouCardParams, + AssigneeParams, + AuthenticationErrorParams, + AutoPayApprovedReportsLimitErrorParams, + BadgeFreeTrialParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartTwo, BeginningOfChatHistoryDomainRoomPartOneParams, + BillingBannerCardAuthenticationRequiredParams, + BillingBannerCardExpiredParams, + BillingBannerCardOnDisputeParams, + BillingBannerDisputePendingParams, + BillingBannerInsufficientFundsParams, + BillingBannerSubtitleWithDateParams, CanceledRequestParams, + CardEndingParams, + CardInfoParams, + CardNextPaymentParams, + CategoryNameParams, ChangeFieldParams, + ChangeOwnerDuplicateSubscriptionParams, + ChangeOwnerHasFailedSettlementsParams, + ChangeOwnerSubscriptionParams, ChangePolicyParams, ChangeTypeParams, + CharacterLengthLimitParams, CharacterLimitParams, CompanyCardFeedNameParams, ConfirmHoldExpenseParams, ConfirmThatParams, + ConnectionNameParams, + ConnectionParams, + CustomersOrJobsLabelParams, + DateParams, DateShouldBeAfterParams, DateShouldBeBeforeParams, + DefaultAmountParams, + DefaultVendorDescriptionParams, + DelegateRoleParams, DelegateSubmitParams, + DelegatorParams, DeleteActionParams, DeleteConfirmationParams, DeleteExpenseTranslationParams, DidSplitAmountMessageParams, + DimensionsCountParams, DistanceRateOperationsParams, EditActionParams, ElectronicFundsParams, - EnglishTranslation, EnterMagicCodeParams, + ExportAgainModalDescriptionParams, ExportedToIntegrationParams, + ExportIntegrationSelectedParams, + FeatureNameParams, + FiltersAmountBetweenParams, FormattedMaxLengthParams, ForwardedAmountParams, GoBackMessageParams, GoToRoomParams, + ImportedTagsMessageParams, + ImportFieldParams, + ImportMembersSuccessfullDescriptionParams, + ImportTagsSuccessfullDescriptionParams, + IncorrectZipFormatParams, InstantSummaryParams, + IntacctMappingTitleParams, + IntegrationExportParams, + IntegrationSyncFailedParams, + InvalidPropertyParams, + InvalidValueParams, IssueVirtualCardParams, + LastSyncAccountingParams, + LastSyncDateParams, LocalTimeParams, LoggedInAsParams, LogSizeParams, @@ -47,12 +93,15 @@ import type { ManagerApprovedParams, MarkedReimbursedParams, MarkReimbursedFromIntegrationParams, + MissingPropertyParams, NoLongerHaveAccessParams, NotAllowedExtensionParams, NotYouParams, OOOEventSummaryFullDayParams, OOOEventSummaryPartialDayParams, + OptionalParam, OurEmailProviderParams, + OwnerOwesAmountParams, PaidElsewhereWithAmountParams, PaidWithExpensifyWithAmountParams, ParentNavigationSummaryParams, @@ -62,21 +111,28 @@ import type { PayerPaidParams, PayerSettledParams, PaySomeoneParams, + ReconciliationWorksParams, ReimbursementRateParams, + RemovedFromApprovalWorkflowParams, RemovedTheRequestParams, + RemoveMemberPromptParams, RemoveMembersWarningPrompt, RenamedRoomActionParams, ReportArchiveReasonsClosedParams, ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams, ReportArchiveReasonsMergedParams, - ReportArchiveReasonsPolicyDeletedParams, ReportArchiveReasonsRemovedFromPolicyParams, + ReportPolicyNameParams, RequestAmountParams, RequestCountParams, RequestedAmountMessageParams, + RequiredFieldParams, ResolutionConstraintsParams, + RoleNamesParams, RoomNameReservedErrorParams, RoomRenamedToParams, + SecondaryLoginParams, + SelectedNumberParams, SetTheDistanceMerchantParams, SetTheRequestParams, SettledAfterAddedBankAccountParams, @@ -85,19 +141,32 @@ import type { SignUpNewFaceCodeParams, SizeExceededParams, SplitAmountParams, + SpreadCategoriesParams, + SpreadFieldNameParams, + SpreadSheetColumnParams, + StatementTitleParams, StepCounterParams, StripePaidParams, + SubscriptionCommitmentParams, + SubscriptionSettingsRenewsOnParams, + SubscriptionSettingsSaveUpToParams, + SubscriptionSizeParams, + SyncStageNameConnectionsParams, TaskCreatedActionParams, + TaxAmountParams, TermsParams, ThreadRequestReportNameParams, ThreadSentMoneyReportNameParams, ToValidateLoginParams, TransferParams, + TrialStartedTitleParams, UnapprovedParams, + UnapproveWithIntegrationWarningParams, UnshareParams, UntilTimeParams, UpdatedTheDistanceMerchantParams, UpdatedTheRequestParams, + UpdateRoleParams, UsePlusButtonParams, UserIsAlreadyMemberParams, UserSplitParams, @@ -122,11 +191,14 @@ import type { WelcomeNoteParams, WelcomeToRoomParams, WeSentYouMagicSignInLinkParams, + WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams, + YourPlanPriceParams, ZipCodeExampleFormatParams, -} from './types'; +} from './params'; +import type {TranslationDeepObject} from './types'; /* eslint-disable max-len */ -export default { +const translations = { common: { cancel: 'Cancelar', dismiss: 'Descartar', @@ -254,7 +326,7 @@ export default { fieldRequired: 'Este campo es obligatorio.', requestModified: 'Esta solicitud está siendo modificada por otro miembro.', characterLimit: ({limit}: CharacterLimitParams) => `Supera el límite de ${limit} caracteres`, - characterLimitExceedCounter: ({length, limit}) => `Se superó el límite de caracteres (${length}/${limit})`, + characterLimitExceedCounter: ({length, limit}: CharacterLengthLimitParams) => `Se superó el límite de caracteres (${length}/${limit})`, dateInvalid: 'Por favor, selecciona una fecha válida.', invalidDateShouldBeFuture: 'Por favor, elige una fecha igual o posterior a hoy.', invalidTimeShouldBeFuture: 'Por favor, elige una hora al menos un minuto en el futuro.', @@ -636,7 +708,7 @@ export default { shouldUseYou ? `Este chat ya no está activo porque tu ya no eres miembro del espacio de trabajo ${policyName}.` : `Este chat está desactivado porque ${displayName} ha dejado de ser miembro del espacio de trabajo ${policyName}.`, - [CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}: ReportArchiveReasonsPolicyDeletedParams) => + [CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}: ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams) => `Este chat está desactivado porque el espacio de trabajo ${policyName} se ha eliminado.`, [CONST.REPORT.ARCHIVE_REASON.INVOICE_RECEIVER_POLICY_DELETED]: ({policyName}: ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams) => `Este chat está desactivado porque el espacio de trabajo ${policyName} se ha eliminado.`, @@ -677,14 +749,14 @@ export default { dragAndDrop: 'Arrastra y suelta un archivo de hoja de cálculo aquí', chooseSpreadsheet: 'Subir', fileContainsHeader: 'El archivo contiene encabezados', - column: (name: string) => `Columna ${name}`, - fieldNotMapped: (fieldName: string) => `¡Vaya! Un campo obligatorio ("${fieldName}") no ha sido mapeado. Por favor, revisa e inténtalo de nuevo.`, - singleFieldMultipleColumns: (fieldName: string) => `¡Vaya! Has mapeado un solo campo ("${fieldName}") a varias columnas. Por favor, revisa e inténtalo de nuevo.`, + column: ({name}: SpreadSheetColumnParams) => `Columna ${name}`, + fieldNotMapped: ({fieldName}: SpreadFieldNameParams) => `¡Vaya! Un campo obligatorio ("${fieldName}") no ha sido mapeado. Por favor, revisa e inténtalo de nuevo.`, + singleFieldMultipleColumns: ({fieldName}: SpreadFieldNameParams) => `¡Vaya! Has mapeado un solo campo ("${fieldName}") a varias columnas. Por favor, revisa e inténtalo de nuevo.`, importFailedTitle: 'Fallo en la importación', importFailedDescription: 'Por favor, asegúrate de que todos los campos estén llenos correctamente e inténtalo de nuevo. Si el problema persiste, por favor contacta a Concierge.', - importCategoriesSuccessfullDescription: (categories: number) => (categories > 1 ? `Se han agregado ${categories} categorías.` : 'Se ha agregado 1 categoría.'), - importMembersSuccessfullDescription: (members: number) => (members > 1 ? `Se han agregado ${members} miembros.` : 'Se ha agregado 1 miembro.'), - importTagsSuccessfullDescription: (tags: number) => (tags > 1 ? `Se han agregado ${tags} etiquetas.` : 'Se ha agregado 1 etiqueta.'), + importCategoriesSuccessfullDescription: ({categories}: SpreadCategoriesParams) => (categories > 1 ? `Se han agregado ${categories} categorías.` : 'Se ha agregado 1 categoría.'), + importMembersSuccessfullDescription: ({members}: ImportMembersSuccessfullDescriptionParams) => (members > 1 ? `Se han agregado ${members} miembros.` : 'Se ha agregado 1 miembro.'), + importTagsSuccessfullDescription: ({tags}: ImportTagsSuccessfullDescriptionParams) => (tags > 1 ? `Se han agregado ${tags} etiquetas.` : 'Se ha agregado 1 etiqueta.'), importSuccessfullTitle: 'Importar categorías', importDescription: 'Elige qué campos mapear desde tu hoja de cálculo haciendo clic en el menú desplegable junto a cada columna importada a continuación.', sizeNotMet: 'El archivo adjunto debe ser más grande que 0 bytes.', @@ -722,7 +794,7 @@ export default { splitBill: 'Dividir gasto', splitScan: 'Dividir recibo', splitDistance: 'Dividir distancia', - paySomeone: (name: string) => `Pagar a ${name ?? 'alguien'}`, + paySomeone: ({name}: PaySomeoneParams = {}) => `Pagar a ${name ?? 'alguien'}`, assignTask: 'Assignar tarea', header: 'Acción rápida', trackManual: 'Crear gasto', @@ -751,7 +823,7 @@ export default { share: 'Compartir', participants: 'Participantes', submitExpense: 'Presentar gasto', - paySomeone: ({name}: PaySomeoneParams) => `Pagar a ${name ?? 'alguien'}`, + paySomeone: ({name}: PaySomeoneParams = {}) => `Pagar a ${name ?? 'alguien'}`, trackExpense: 'Seguimiento de gastos', pay: 'Pagar', cancelPayment: 'Cancelar el pago', @@ -765,7 +837,7 @@ export default { pendingMatchWithCreditCardDescription: 'Recibo pendiente de adjuntar con la transacción de la tarjeta. Márcalo como efectivo para cancelar.', markAsCash: 'Marcar como efectivo', routePending: 'Ruta pendiente...', - receiptIssuesFound: (count: number) => `${count === 1 ? 'Problema encontrado' : 'Problemas encontrados'}`, + receiptIssuesFound: ({count}: DistanceRateOperationsParams) => `${count === 1 ? 'Problema encontrado' : 'Problemas encontrados'}`, fieldPending: 'Pendiente...', receiptScanning: 'Escaneando recibo...', receiptScanInProgress: 'Escaneado de recibo en proceso', @@ -812,6 +884,8 @@ export default { sendInvoice: ({amount}: RequestAmountParams) => `Enviar factura de ${amount}`, submitAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`, submittedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `solicitó ${formattedAmount}${comment ? ` para ${comment}` : ''}`, + automaticallySubmittedAmount: ({formattedAmount}: RequestedAmountMessageParams) => + `se enviaron automáticamente ${formattedAmount} mediante envío diferido`, trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `realizó un seguimiento de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, splitAmount: ({amount}: SplitAmountParams) => `dividir ${amount}`, didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `dividió ${formattedAmount}${comment ? ` para ${comment}` : ''}`, @@ -820,7 +894,7 @@ export default { payerOwes: ({payer}: PayerOwesParams) => `${payer} debe: `, payerPaidAmount: ({payer, amount}: PayerPaidAmountParams) => `${payer ? `${payer} ` : ''}pagó ${amount}`, payerPaid: ({payer}: PayerPaidParams) => `${payer} pagó: `, - payerSpentAmount: ({payer, amount}: PayerPaidAmountParams): string => `${payer} gastó ${amount}`, + payerSpentAmount: ({payer, amount}: PayerPaidAmountParams) => `${payer} gastó ${amount}`, payerSpent: ({payer}: PayerPaidParams) => `${payer} gastó: `, managerApproved: ({manager}: ManagerApprovedParams) => `${manager} aprobó:`, managerApprovedAmount: ({manager, amount}: ManagerApprovedAmountParams) => `${manager} aprobó ${amount}`, @@ -928,7 +1002,7 @@ export default { unapprove: 'Desaprobar', unapproveReport: 'Anular la aprobación del informe', headsUp: 'Atención!', - unapproveWithIntegrationWarning: (accountingIntegration: string) => + unapproveWithIntegrationWarning: ({accountingIntegration}: UnapproveWithIntegrationWarningParams) => `Este informe ya se ha exportado a ${accountingIntegration}. Los cambios realizados en este informe en Expensify pueden provocar discrepancias en los datos y problemas de conciliación de la tarjeta Expensify. ¿Está seguro de que desea anular la aprobación de este informe?`, reimbursable: 'reembolsable', nonReimbursable: 'no reembolsable', @@ -1315,15 +1389,15 @@ export default { availableSpend: 'Límite restante', smartLimit: { name: 'Límite inteligente', - title: (formattedLimit: string) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta al mes. El límite se restablecerá el primer día del mes.`, + title: ({formattedLimit}: ViolationsOverLimitParams) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta al mes. El límite se restablecerá el primer día del mes.`, }, fixedLimit: { name: 'Límite fijo', - title: (formattedLimit: string) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta, luego se desactivará.`, + title: ({formattedLimit}: ViolationsOverLimitParams) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta, luego se desactivará.`, }, monthlyLimit: { name: 'Límite mensual', - title: (formattedLimit: string) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta y el límite se restablecerá a medida que se aprueben tus gastos.`, + title: ({formattedLimit}: ViolationsOverLimitParams) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta y el límite se restablecerá a medida que se aprueben tus gastos.`, }, virtualCardNumber: 'Número de la tarjeta virtual', physicalCardNumber: 'Número de la tarjeta física', @@ -1527,7 +1601,7 @@ export default { }, }, reportDetailsPage: { - inWorkspace: ({policyName}) => `en ${policyName}`, + inWorkspace: ({policyName}: ReportPolicyNameParams) => `en ${policyName}`, }, reportDescriptionPage: { roomDescription: 'Descripción de la sala de chat', @@ -1540,7 +1614,7 @@ export default { groupChat: { lastMemberTitle: '¡Atención!', lastMemberWarning: 'Ya que eres la última persona aquí, si te vas, este chat quedará inaccesible para todos los miembros. ¿Estás seguro de que quieres salir del chat?', - defaultReportName: ({displayName}: {displayName: string}) => `Chat de grupo de ${displayName}`, + defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `Chat de grupo de ${displayName}`, }, languagePage: { language: 'Idioma', @@ -1671,7 +1745,7 @@ export default { error: { dateShouldBeBefore: ({dateString}: DateShouldBeBeforeParams) => `La fecha debe ser anterior a ${dateString}.`, dateShouldBeAfter: ({dateString}: DateShouldBeAfterParams) => `La fecha debe ser posterior a ${dateString}.`, - incorrectZipFormat: (zipFormat?: string) => `Formato de código postal incorrecto.${zipFormat ? ` Formato aceptable: ${zipFormat}` : ''}`, + incorrectZipFormat: ({zipFormat}: IncorrectZipFormatParams = {}) => `Formato de código postal incorrecto.${zipFormat ? ` Formato aceptable: ${zipFormat}` : ''}`, hasInvalidCharacter: 'El nombre sólo puede incluir caracteres latinos.', }, }, @@ -2228,7 +2302,7 @@ export default { testTransactions: 'Transacciones de prueba', issueAndManageCards: 'Emitir y gestionar tarjetas', reconcileCards: 'Reconciliar tarjetas', - selected: ({selectedNumber}) => `${selectedNumber} seleccionados`, + selected: ({selectedNumber}: SelectedNumberParams) => `${selectedNumber} seleccionados`, settlementFrequency: 'Frecuencia de liquidación', deleteConfirmation: '¿Estás seguro de que quieres eliminar este espacio de trabajo?', unavailable: 'Espacio de trabajo no disponible', @@ -2247,7 +2321,7 @@ export default { `¡Has sido invitado a ${workspaceName}! Descargue la aplicación móvil Expensify en use.expensify.com/download para comenzar a rastrear sus gastos.`, subscription: 'Suscripción', markAsExported: 'Marcar como introducido manualmente', - exportIntegrationSelected: (connectionName: ConnectionName) => `Exportar a ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`, + exportIntegrationSelected: ({connectionName}: ExportIntegrationSelectedParams) => `Exportar a ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`, letsDoubleCheck: 'Verifiquemos que todo esté correcto', reportField: 'Campo del informe', lineItemLevel: 'Nivel de partida', @@ -2264,14 +2338,14 @@ export default { createNewConnection: 'Crear una nueva conexión', reuseExistingConnection: 'Reutilizar la conexión existente', existingConnections: 'Conexiones existentes', - lastSyncDate: (connectionName: string, formattedDate: string) => `${connectionName} - Última sincronización ${formattedDate}`, + lastSyncDate: ({connectionName, formattedDate}: LastSyncDateParams) => `${connectionName} - Última sincronización ${formattedDate}`, topLevel: 'Nivel superior', - authenticationError: (connectionName: string) => `No se puede conectar a ${connectionName} debido a un error de autenticación.`, + authenticationError: ({connectionName}: AuthenticationErrorParams) => `No se puede conectar a ${connectionName} debido a un error de autenticación.`, learnMore: 'Más información.', memberAlternateText: 'Los miembros pueden presentar y aprobar informes.', adminAlternateText: 'Los administradores tienen acceso total para editar todos los informes y la configuración del área de trabajo.', auditorAlternateText: 'Los auditores pueden ver y comentar los informes.', - roleName: (role?: string): string => { + roleName: ({role}: OptionalParam = {}) => { switch (role) { case CONST.POLICY.ROLE.ADMIN: return 'Administrador'; @@ -2402,8 +2476,8 @@ export default { accountsSwitchDescription: 'Las categorías activas estarán disponibles para ser escogidas cuando se crea un gasto.', trackingCategories: 'Categorías de seguimiento', trackingCategoriesDescription: 'Elige cómo gestionar categorías de seguimiento de Xero en Expensify.', - mapTrackingCategoryTo: ({categoryName}) => `Asignar ${categoryName} de Xero a`, - mapTrackingCategoryToDescription: ({categoryName}) => `Elige dónde mapear ${categoryName} al exportar a Xero.`, + mapTrackingCategoryTo: ({categoryName}: CategoryNameParams) => `Asignar ${categoryName} de Xero a`, + mapTrackingCategoryToDescription: ({categoryName}: CategoryNameParams) => `Elige dónde mapear ${categoryName} al exportar a Xero.`, customers: 'Volver a facturar a los clientes', customersDescription: 'Elige si quieres volver a facturar a los clientes en Expensify. Tus contactos de clientes de Xero se pueden etiquetar como gastos, y se exportarán a Xero como una factura de venta.', @@ -2504,7 +2578,7 @@ export default { }, creditCardAccount: 'Cuenta de tarjeta de crédito', defaultVendor: 'Proveedor por defecto', - defaultVendorDescription: (isReimbursable: boolean): string => + defaultVendorDescription: ({isReimbursable}: DefaultVendorDescriptionParams) => `Establezca un proveedor predeterminado que se aplicará a los gastos ${isReimbursable ? '' : 'no '}reembolsables que no tienen un proveedor coincidente en Sage Intacct.`, exportDescription: 'Configure cómo se exportan los datos de Expensify a Sage Intacct.', exportPreferredExporterNote: @@ -2722,12 +2796,12 @@ export default { importJobs: 'Importar proyectos', customers: 'clientes', jobs: 'proyectos', - label: (importFields: string[], importType: string) => `${importFields.join(' y ')}, ${importType}`, + label: ({importFields, importType}: CustomersOrJobsLabelParams) => `${importFields.join(' y ')}, ${importType}`, }, importTaxDescription: 'Importar grupos de impuestos desde NetSuite.', importCustomFields: { chooseOptionBelow: 'Elija una de las opciones siguientes:', - requiredFieldError: (fieldName: string) => `Por favor, introduzca el ${fieldName}`, + requiredFieldError: ({fieldName}: RequiredFieldParams) => `Por favor, introduzca el ${fieldName}`, customSegments: { title: 'Segmentos/registros personalizados', addText: 'Añadir segmento/registro personalizado', @@ -2768,7 +2842,7 @@ export default { customRecordMappingTitle: '¿Cómo debería mostrarse este registro de segmento personalizado en Expensify?', }, errors: { - uniqueFieldError: (fieldName: string) => `Ya existe un segmento/registro personalizado con este ${fieldName?.toLowerCase()}.`, + uniqueFieldError: ({fieldName}: RequiredFieldParams) => `Ya existe un segmento/registro personalizado con este ${fieldName?.toLowerCase()}.`, }, }, customLists: { @@ -2802,18 +2876,18 @@ export default { [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: { label: 'Predeterminado del empleado NetSuite', description: 'No importado a Expensify, aplicado en exportación', - footerContent: (importField: string) => + footerContent: ({importField}: ImportFieldParams) => `Si usa ${importField} en NetSuite, aplicaremos el conjunto predeterminado en el registro del empleado al exportarlo a Informe de gastos o Entrada de diario.`, }, [CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG]: { label: 'Etiquetas', description: 'Nivel de línea de pedido', - footerContent: (importField: string) => `Se podrán seleccionar ${importField} para cada gasto individual en el informe de un empleado.`, + footerContent: ({importField}: ImportFieldParams) => `Se podrán seleccionar ${importField} para cada gasto individual en el informe de un empleado.`, }, [CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: { label: 'Campos de informe', description: 'Nivel de informe', - footerContent: (importField: string) => `La selección de ${importField} se aplicará a todos los gastos en el informe de un empleado.`, + footerContent: ({importField}: ImportFieldParams) => `La selección de ${importField} se aplicará a todos los gastos en el informe de un empleado.`, }, }, }, @@ -2844,8 +2918,8 @@ export default { addAUserDefinedDimension: 'Añadir una dimensión definida por el usuario', detailedInstructionsLink: 'Ver instrucciones detalladas', detailedInstructionsRestOfSentence: ' para añadir dimensiones definidas por el usuario.', - userDimensionsAdded: (dimensionsCount: number) => `${dimensionsCount} ${Str.pluralize('UDD', `UDDs`, dimensionsCount)} añadido`, - mappingTitle: (mappingName: SageIntacctMappingName): string => { + userDimensionsAdded: ({dimensionsCount}: DimensionsCountParams) => `${dimensionsCount} ${Str.pluralize('UDD', `UDDs`, dimensionsCount)} añadido`, + mappingTitle: ({mappingName}: IntacctMappingTitleParams) => { switch (mappingName) { case CONST.SAGE_INTACCT_CONFIG.MAPPINGS.DEPARTMENTS: return 'departamentos'; @@ -2879,7 +2953,7 @@ export default { }, yourCardProvider: `¿Quién es su proveedor de tarjetas?`, enableFeed: { - title: (provider: string) => `Habilita tu feed ${provider}`, + title: ({provider}: GoBackMessageParams) => `Habilita tu feed ${provider}`, heading: 'Tenemos una integración directa con el emisor de su tarjeta y podemos importar los datos de sus transacciones a Expensify de forma rápida y precisa.\n\nPara empezar, simplemente:', visa: `1. Visite [este artículo de ayuda](${CONST.COMPANY_CARDS_HELP}) para obtener instrucciones detalladas sobre cómo configurar sus tarjetas comerciales Visa.\n\n2. [Póngase en contacto con su banco](${CONST.COMPANY_CARDS_HELP}) para comprobar que admiten un feed personalizado para su programa, y pídales que lo activen.\n\n3. *Una vez que el feed esté habilitado y tengas sus datos, pasa a la siguiente pantalla.*`, @@ -2927,7 +3001,7 @@ export default { card: 'Tarjeta', startTransactionDate: 'Fecha de inicio de transacciones', cardName: 'Nombre de la tarjeta', - assignedYouCard: (assigner: string) => `¡${assigner} te ha asignado una tarjeta de empresa! Las transacciones importadas aparecerán en este chat.`, + assignedYouCard: ({assigner}: AssignedYouCardParams) => `¡${assigner} te ha asignado una tarjeta de empresa! Las transacciones importadas aparecerán en este chat.`, chooseCardFeed: 'Elige feed de tarjetas', }, expensifyCard: { @@ -2975,21 +3049,21 @@ export default { deactivate: 'Desactivar tarjeta', changeCardLimit: 'Modificar el límite de la tarjeta', changeLimit: 'Modificar límite', - smartLimitWarning: (limit: string) => + smartLimitWarning: ({limit}: CharacterLimitParams) => `Si cambias el límite de esta tarjeta a ${limit}, las nuevas transacciones serán rechazadas hasta que apruebes antiguos gastos de la tarjeta.`, - monthlyLimitWarning: (limit: string) => `Si cambias el límite de esta tarjeta a ${limit}, las nuevas transacciones serán rechazadas hasta el próximo mes.`, - fixedLimitWarning: (limit: string) => `Si cambias el límite de esta tarjeta a ${limit}, se rechazarán las nuevas transacciones.`, + monthlyLimitWarning: ({limit}: CharacterLimitParams) => `Si cambias el límite de esta tarjeta a ${limit}, las nuevas transacciones serán rechazadas hasta el próximo mes.`, + fixedLimitWarning: ({limit}: CharacterLimitParams) => `Si cambias el límite de esta tarjeta a ${limit}, se rechazarán las nuevas transacciones.`, changeCardLimitType: 'Modificar el tipo de límite de la tarjeta', changeLimitType: 'Modificar el tipo de límite', - changeCardSmartLimitTypeWarning: (limit: string) => + changeCardSmartLimitTypeWarning: ({limit}: CharacterLimitParams) => `Si cambias el tipo de límite de esta tarjeta a Límite inteligente, las nuevas transacciones serán rechazadas porque ya se ha alcanzado el límite de ${limit} no aprobado.`, - changeCardMonthlyLimitTypeWarning: (limit: string) => + changeCardMonthlyLimitTypeWarning: ({limit}: CharacterLimitParams) => `Si cambias el tipo de límite de esta tarjeta a Mensual, las nuevas transacciones serán rechazadas porque ya se ha alcanzado el límite de ${limit} mensual.`, addShippingDetails: 'Añadir detalles de envío', - issuedCard: (assignee: string) => `¡emitió a ${assignee} una Tarjeta Expensify! La tarjeta llegará en 2-3 días laborables.`, - issuedCardNoShippingDetails: (assignee: string) => `¡emitió a ${assignee} una Tarjeta Expensify! La tarjeta se enviará una vez que se agreguen los detalles de envío.`, + issuedCard: ({assignee}: AssigneeParams) => `¡emitió a ${assignee} una Tarjeta Expensify! La tarjeta llegará en 2-3 días laborables.`, + issuedCardNoShippingDetails: ({assignee}: AssigneeParams) => `¡emitió a ${assignee} una Tarjeta Expensify! La tarjeta se enviará una vez que se agreguen los detalles de envío.`, issuedCardVirtual: ({assignee, link}: IssueVirtualCardParams) => `¡emitió a ${assignee} una ${link} virtual! La tarjeta puede utilizarse inmediatamente.`, - addedShippingDetails: (assignee: string) => `${assignee} agregó los detalles de envío. La Tarjeta Expensify llegará en 2-3 días hábiles.`, + addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} agregó los detalles de envío. La Tarjeta Expensify llegará en 2-3 días hábiles.`, }, categories: { deleteCategories: 'Eliminar categorías', @@ -3088,8 +3162,9 @@ export default { cardNumber: 'Número de la tarjeta', cardholder: 'Titular de la tarjeta', cardName: 'Nombre de la tarjeta', - integrationExport: (integration: string, type: string) => `Exportación a ${integration} ${type}`, - integrationExportTitleFirstPart: (integration: string) => `Seleccione la cuenta ${integration} donde se deben exportar las transacciones. Seleccione una cuenta diferente`, + integrationExport: ({integration, type}: IntegrationExportParams) => `Exportación a ${integration} ${type}`, + integrationExportTitleFirstPart: ({integration}: IntegrationExportParams) => + `Seleccione la cuenta ${integration} donde se deben exportar las transacciones. Seleccione una cuenta diferente`, integrationExportTitleLinkPart: 'opción de exportación', integrationExportTitleSecondPart: 'para cambiar las cuentas disponibles.', lastUpdated: 'Última actualización', @@ -3123,7 +3198,7 @@ export default { giveItNameInstruction: 'Nombra la tarjeta para distingirla de las demás.', updating: 'Actualizando...', noAccountsFound: 'No se han encontrado cuentas', - noAccountsFoundDescription: (connection: string) => `Añade la cuenta en ${connection} y sincroniza la conexión de nuevo.`, + noAccountsFoundDescription: ({connection}: ConnectionParams) => `Añade la cuenta en ${connection} y sincroniza la conexión de nuevo.`, }, workflows: { title: 'Flujos de trabajo', @@ -3244,7 +3319,7 @@ export default { tagRules: 'Reglas de etiquetas', approverDescription: 'Aprobador', importTags: 'Importar categorías', - importedTagsMessage: (columnCounts: number) => + importedTagsMessage: ({columnCounts}: ImportedTagsMessageParams) => `Hemos encontrado *${columnCounts} columnas* en su hoja de cálculo. Seleccione *Nombre* junto a la columna que contiene los nombres de las etiquetas. También puede seleccionar *Habilitado* junto a la columna que establece el estado de la etiqueta.`, }, taxes: { @@ -3267,7 +3342,7 @@ export default { updateTaxClaimableFailureMessage: 'La porción recuperable debe ser menor al monto del importe por distancia.', }, deleteTaxConfirmation: '¿Estás seguro de que quieres eliminar este impuesto?', - deleteMultipleTaxConfirmation: ({taxAmount}) => `¿Estás seguro de que quieres eliminar ${taxAmount} impuestos?`, + deleteMultipleTaxConfirmation: ({taxAmount}: TaxAmountParams) => `¿Estás seguro de que quieres eliminar ${taxAmount} impuestos?`, actions: { delete: 'Eliminar tasa', deleteMultiple: 'Eliminar tasas', @@ -3310,7 +3385,7 @@ export default { removeWorkspaceMemberButtonTitle: 'Eliminar del espacio de trabajo', removeGroupMemberButtonTitle: 'Eliminar del grupo', removeRoomMemberButtonTitle: 'Eliminar del chat', - removeMemberPrompt: ({memberName}: {memberName: string}) => `¿Estás seguro de que deseas eliminar a ${memberName}?`, + removeMemberPrompt: ({memberName}: RemoveMemberPromptParams) => `¿Estás seguro de que deseas eliminar a ${memberName}?`, removeMemberTitle: 'Eliminar miembro', transferOwner: 'Transferir la propiedad', makeMember: 'Hacer miembro', @@ -3323,7 +3398,7 @@ export default { genericRemove: 'Ha ocurrido un problema al eliminar al miembro del espacio de trabajo.', }, addedWithPrimary: 'Se agregaron algunos miembros con sus nombres de usuario principales.', - invitedBySecondaryLogin: ({secondaryLogin}) => `Agregado por nombre de usuario secundario ${secondaryLogin}.`, + invitedBySecondaryLogin: ({secondaryLogin}: SecondaryLoginParams) => `Agregado por nombre de usuario secundario ${secondaryLogin}.`, membersListTitle: 'Directorio de todos los miembros del espacio de trabajo.', importMembers: 'Importar miembros', }, @@ -3335,8 +3410,8 @@ export default { xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', - connectionName: (integration: ConnectionName) => { - switch (integration) { + connectionName: ({connectionName}: ConnectionNameParams) => { + switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: return 'Quickbooks Online'; case CONST.POLICY.CONNECTIONS.NAME.XERO: @@ -3353,20 +3428,21 @@ export default { errorODIntegration: 'Hay un error con una conexión que se ha configurado en Expensify Classic. ', goToODToFix: 'Ve a Expensify Classic para solucionar este problema.', setup: 'Configurar', - lastSync: (relativeDate: string) => `Recién sincronizado ${relativeDate}`, + lastSync: ({relativeDate}: LastSyncAccountingParams) => `Recién sincronizado ${relativeDate}`, import: 'Importar', export: 'Exportar', advanced: 'Avanzado', other: 'Otras integraciones', syncNow: 'Sincronizar ahora', disconnect: 'Desconectar', - disconnectTitle: (integration?: ConnectionName): string => { - const integrationName = integration && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integration] ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integration] : 'integración'; + disconnectTitle: ({connectionName}: OptionalParam = {}) => { + const integrationName = + connectionName && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] : 'integración'; return `Desconectar ${integrationName}`; }, - connectTitle: (integrationToConnect: ConnectionName): string => `Conectar ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integrationToConnect] ?? 'accounting integration'}`, - syncError: (integration?: ConnectionName): string => { - switch (integration) { + connectTitle: ({connectionName}: ConnectionNameParams) => `Conectar ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ?? 'accounting integration'}`, + syncError: ({connectionName}: OptionalParam = {}) => { + switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: return 'No se puede conectar a QuickBooks Online.'; case CONST.POLICY.CONNECTIONS.NAME.XERO: @@ -3392,18 +3468,18 @@ export default { [CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: 'Importado como campos de informe', [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Predeterminado del empleado NetSuite', }, - disconnectPrompt: (currentIntegration?: ConnectionName): string => { + disconnectPrompt: ({connectionName}: OptionalParam = {}) => { const integrationName = - currentIntegration && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[currentIntegration] ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[currentIntegration] : 'integración'; + connectionName && CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] : 'integración'; return `¿Estás seguro de que quieres desconectar ${integrationName}?`; }, - connectPrompt: (integrationToConnect: ConnectionName): string => + connectPrompt: ({connectionName}: ConnectionNameParams) => `¿Estás seguro de que quieres conectar a ${ - CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integrationToConnect] ?? 'esta integración contable' + CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ?? 'esta integración contable' }? Esto eliminará cualquier conexión contable existente.`, enterCredentials: 'Ingresa tus credenciales', connections: { - syncStageName: (stage: PolicyConnectionSyncStage) => { + syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'quickbooksOnlineImportCustomers': return 'Importando clientes'; @@ -3540,7 +3616,7 @@ export default { chooseBankAccount: 'Elige la cuenta bancaria con la que se conciliarán los pagos de tu Tarjeta Expensify.', accountMatches: 'Asegúrate de que esta cuenta coincide con ', settlementAccount: 'la cuenta de liquidación de tu Tarjeta Expensify ', - reconciliationWorks: (lastFourPAN: string) => `(que termina en ${lastFourPAN}) para que la conciliación continua funcione correctamente.`, + reconciliationWorks: ({lastFourPAN}: ReconciliationWorksParams) => `(que termina en ${lastFourPAN}) para que la conciliación continua funcione correctamente.`, }, }, card: { @@ -3638,7 +3714,10 @@ export default { rate: 'Tasa', addRate: 'Agregar tasa', trackTax: 'Impuesto de seguimiento', - deleteRates: ({count}: DistanceRateOperationsParams) => `Eliminar ${Str.pluralize('tasa', 'tasas', count)}`, + deleteRates: () => ({ + one: 'Eliminar tasa', + other: 'Eliminar tasas', + }), enableRates: ({count}: DistanceRateOperationsParams) => `Activar ${Str.pluralize('tasa', 'tasas', count)}`, disableRates: ({count}: DistanceRateOperationsParams) => `Desactivar ${Str.pluralize('tasa', 'tasas', count)}`, enableRate: 'Activar tasa', @@ -3708,19 +3787,19 @@ export default { amountOwedText: 'Esta cuenta tiene un saldo pendiente de un mes anterior.\n\n¿Quiere liquidar el saldo y hacerse cargo de la facturación de este espacio de trabajo?', ownerOwesAmountTitle: 'Saldo pendiente', ownerOwesAmountButtonText: 'Transferir saldo', - ownerOwesAmountText: ({email, amount}) => + ownerOwesAmountText: ({email, amount}: OwnerOwesAmountParams) => `La cuenta propietaria de este espacio de trabajo (${email}) tiene un saldo pendiente de un mes anterior.\n\n¿Desea transferir este monto (${amount}) para hacerse cargo de la facturación de este espacio de trabajo? tu tarjeta de pago se cargará inmediatamente.`, subscriptionTitle: 'Asumir la suscripción anual', subscriptionButtonText: 'Transferir suscripción', - subscriptionText: ({usersCount, finalCount}) => + subscriptionText: ({usersCount, finalCount}: ChangeOwnerSubscriptionParams) => `Al hacerse cargo de este espacio de trabajo se fusionará tu suscripción anual asociada con tu suscripción actual. Esto aumentará el tamaño de tu suscripción en ${usersCount} miembros, lo que hará que tu nuevo tamaño de suscripción sea ${finalCount}. ¿Te gustaria continuar?`, duplicateSubscriptionTitle: 'Alerta de suscripción duplicada', duplicateSubscriptionButtonText: 'Continuar', - duplicateSubscriptionText: ({email, workspaceName}) => + duplicateSubscriptionText: ({email, workspaceName}: ChangeOwnerDuplicateSubscriptionParams) => `Parece que estás intentando hacerte cargo de la facturación de los espacios de trabajo de ${email}, pero para hacerlo, primero debes ser administrador de todos sus espacios de trabajo.\n\nHaz clic en "Continuar" si solo quieres tomar sobrefacturación para el espacio de trabajo ${workspaceName}.\n\nSi desea hacerse cargo de la facturación de toda tu suscripción, pídales que lo agreguen como administrador a todos sus espacios de trabajo antes de hacerse cargo de la facturación.`, hasFailedSettlementsTitle: 'No se puede transferir la propiedad', hasFailedSettlementsButtonText: 'Entiendo', - hasFailedSettlementsText: ({email}) => + hasFailedSettlementsText: ({email}: ChangeOwnerHasFailedSettlementsParams) => `No puede hacerse cargo de la facturación porque ${email} tiene una liquidación vencida de la tarjeta Expensify. Avíseles que se comuniquen con concierge@expensify.com para resolver el problema. Luego, podrá hacerse cargo de la facturación de este espacio de trabajo.`, failedToClearBalanceTitle: 'Fallo al liquidar el saldo', failedToClearBalanceButtonText: 'OK', @@ -3735,7 +3814,7 @@ export default { exportAgainModal: { title: '¡Cuidado!', - description: (reportName: string, connectionName: ConnectionName) => + description: ({reportName, connectionName}: ExportAgainModalDescriptionParams) => `Los siguientes informes ya se han exportado a ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}:\n\n${reportName}\n\n¿Estás seguro de que deseas exportarlos de nuevo?`, confirmText: 'Sí, exportar de nuevo', cancelText: 'Cancelar', @@ -3798,7 +3877,7 @@ export default { upgradeToUnlock: 'Desbloquear esta función', completed: { headline: 'Has mejorado tu espacio de trabajo.', - successMessage: (policyName: string) => `Ha mejorado correctamente su espacio de trabajo ${policyName} al plan Control.`, + successMessage: ({policyName}: ReportPolicyNameParams) => `Ha mejorado correctamente su espacio de trabajo ${policyName} al plan Control.`, viewSubscription: 'Ver su suscripción', moreDetails: 'para obtener más información.', gotIt: 'Entendido, gracias.', @@ -3806,8 +3885,8 @@ export default { }, restrictedAction: { restricted: 'Restringido', - actionsAreCurrentlyRestricted: ({workspaceName}) => `Las acciones en el espacio de trabajo ${workspaceName} están actualmente restringidas`, - workspaceOwnerWillNeedToAddOrUpdatePaymentCard: ({workspaceOwnerName}) => + actionsAreCurrentlyRestricted: ({workspaceName}: ActionsAreCurrentlyRestricted) => `Las acciones en el espacio de trabajo ${workspaceName} están actualmente restringidas`, + workspaceOwnerWillNeedToAddOrUpdatePaymentCard: ({workspaceOwnerName}: WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams) => `El propietario del espacio de trabajo, ${workspaceOwnerName} tendrá que añadir o actualizar la tarjeta de pago registrada para desbloquear nueva actividad en el espacio de trabajo.`, youWillNeedToAddOrUpdatePaymentCard: 'Debes añadir o actualizar la tarjeta de pago registrada para desbloquear nueva actividad en el espacio de trabajo.', addPaymentCardToUnlock: 'Añade una tarjeta para desbloquearlo!', @@ -3828,7 +3907,7 @@ export default { maxAge: 'Antigüedad máxima', maxExpenseAge: 'Antigüedad máxima de los gastos', maxExpenseAgeDescription: 'Marca los gastos de más de un número determinado de días.', - maxExpenseAgeDays: (age: number) => `${age} ${Str.pluralize('día', 'días', age)}`, + maxExpenseAgeDays: ({age}: AgeParams) => `${age} ${Str.pluralize('día', 'días', age)}`, billableDefault: 'Valor predeterminado facturable', billableDefaultDescription: 'Elige si los gastos en efectivo y con tarjeta de crédito deben ser facturables por defecto. Los gastos facturables se activan o desactivan en', billable: 'Facturable', @@ -3865,26 +3944,26 @@ export default { randomReportAuditDescription: 'Requiere que algunos informes sean aprobados manualmente, incluso si son elegibles para la aprobación automática.', autoPayApprovedReportsTitle: 'Pago automático de informes aprobados', autoPayApprovedReportsSubtitle: 'Configura qué informes de gastos pueden pagarse de forma automática.', - autoPayApprovedReportsLimitError: (currency?: string) => `Por favor, introduce un monto menor a ${currency ?? ''}20,000`, + autoPayApprovedReportsLimitError: ({currency}: AutoPayApprovedReportsLimitErrorParams = {}) => `Por favor, introduce un monto menor a ${currency ?? ''}20,000`, autoPayApprovedReportsLockedSubtitle: 'Ve a más funciones y habilita flujos de trabajo, luego agrega pagos para desbloquear esta función.', autoPayReportsUnderTitle: 'Pagar automáticamente informes por debajo de', autoPayReportsUnderDescription: 'Los informes de gastos totalmente conformes por debajo de esta cantidad se pagarán automáticamente.', unlockFeatureGoToSubtitle: 'Ir a', - unlockFeatureEnableWorkflowsSubtitle: (featureName: string) => `y habilita flujos de trabajo, luego agrega ${featureName} para desbloquear esta función.`, - enableFeatureSubtitle: (featureName: string) => `y habilita ${featureName} para desbloquear esta función.`, + unlockFeatureEnableWorkflowsSubtitle: ({featureName}: FeatureNameParams) => `y habilita flujos de trabajo, luego agrega ${featureName} para desbloquear esta función.`, + enableFeatureSubtitle: ({featureName}: FeatureNameParams) => `y habilita ${featureName} para desbloquear esta función.`, }, categoryRules: { title: 'Reglas de categoría', approver: 'Aprobador', requireDescription: 'Requerir descripción', descriptionHint: 'Sugerencia de descripción', - descriptionHintDescription: (categoryName: string) => + descriptionHintDescription: ({categoryName}: CategoryNameParams) => `Recuerda a los empleados que deben proporcionar información adicional para los gastos de “${categoryName}”. Esta sugerencia aparece en el campo de descripción en los gastos.`, descriptionHintLabel: 'Sugerencia', descriptionHintSubtitle: 'Consejo: ¡Cuanto más corta, mejor!', maxAmount: 'Importe máximo', flagAmountsOver: 'Señala importes superiores a', - flagAmountsOverDescription: (categoryName: string) => `Aplica a la categoría “${categoryName}”.`, + flagAmountsOverDescription: ({categoryName}: CategoryNameParams) => `Aplica a la categoría “${categoryName}”.`, flagAmountsOverSubtitle: 'Esto anula el importe máximo para todos los gastos.', expenseLimitTypes: { expense: 'Gasto individual', @@ -3894,7 +3973,7 @@ export default { }, requireReceiptsOver: 'Requerir recibos para importes superiores a', requireReceiptsOverList: { - default: (defaultAmount: string) => `${defaultAmount} ${CONST.DOT_SEPARATOR} Predeterminado`, + default: ({defaultAmount}: DefaultAmountParams) => `${defaultAmount} ${CONST.DOT_SEPARATOR} Predeterminado`, never: 'Nunca requerir recibos', always: 'Requerir recibos siempre', }, @@ -3958,8 +4037,8 @@ export default { }, }, workspaceActions: { - renamedWorkspaceNameAction: ({oldName, newName}) => `actualizó el nombre de este espacio de trabajo de ${oldName} a ${newName}`, - removedFromApprovalWorkflow: ({submittersNames}: {submittersNames: string[]}) => { + renamedWorkspaceNameAction: ({oldName, newName}: RenamedRoomActionParams) => `actualizó el nombre de este espacio de trabajo de ${oldName} a ${newName}`, + removedFromApprovalWorkflow: ({submittersNames}: RemovedFromApprovalWorkflowParams) => { let joinedNames = ''; if (submittersNames.length === 1) { joinedNames = submittersNames[0]; @@ -4012,7 +4091,7 @@ export default { deleteConfirmation: '¿Estás seguro de que quieres eliminar esta tarea?', }, statementPage: { - title: (year, monthName) => `Estado de cuenta de ${monthName} ${year}`, + title: ({year, monthName}: StatementTitleParams) => `Estado de cuenta de ${monthName} ${year}`, generatingPDF: 'Estamos generando tu PDF ahora mismo. ¡Por favor, vuelve más tarde!', }, keyboardShortcutsPage: { @@ -4062,8 +4141,8 @@ export default { filtersHeader: 'Filtros', filters: { date: { - before: (date?: string) => `Antes de ${date ?? ''}`, - after: (date?: string) => `Después de ${date ?? ''}`, + before: ({date}: OptionalParam = {}) => `Antes de ${date ?? ''}`, + after: ({date}: OptionalParam = {}) => `Después de ${date ?? ''}`, }, status: 'Estado', keyword: 'Palabra clave', @@ -4073,9 +4152,9 @@ export default { pinned: 'Fijado', unread: 'No leído', amount: { - lessThan: (amount?: string) => `Menos de ${amount ?? ''}`, - greaterThan: (amount?: string) => `Más que ${amount ?? ''}`, - between: (greaterThan: string, lessThan: string) => `Entre ${greaterThan} y ${lessThan}`, + lessThan: ({amount}: OptionalParam = {}) => `Menos de ${amount ?? ''}`, + greaterThan: ({amount}: OptionalParam = {}) => `Más que ${amount ?? ''}`, + between: ({greaterThan, lessThan}: FiltersAmountBetweenParams) => `Entre ${greaterThan} y ${lessThan}`, }, current: 'Actual', past: 'Anterior', @@ -4196,7 +4275,7 @@ export default { nonReimbursableLink: 'Ver los gastos de la tarjeta de empresa.', pending: ({label}: ExportedToIntegrationParams) => `comenzó a exportar este informe a ${label}...`, }, - integrationsMessage: (errorMessage: string, label: string) => `no se pudo exportar este informe a ${label} ("${errorMessage}").`, + integrationsMessage: ({label, errorMessage}: IntegrationSyncFailedParams) => `no se pudo exportar este informe a ${label} ("${errorMessage}").`, managerAttachReceipt: `agregó un recibo`, managerDetachReceipt: `quitó un recibo`, markedReimbursed: ({amount, currency}: MarkedReimbursedParams) => `pagó ${currency}${amount} en otro lugar`, @@ -4213,11 +4292,11 @@ export default { stripePaid: ({amount, currency}: StripePaidParams) => `pagado ${currency}${amount}`, takeControl: `tomó el control`, unapproved: ({amount, currency}: UnapprovedParams) => `no aprobado ${currency}${amount}`, - integrationSyncFailed: (label: string, errorMessage: string) => `no se pudo sincronizar con ${label} ("${errorMessage}")`, - addEmployee: (email: string, role: string) => `agregó a ${email} como ${role === 'user' ? 'miembro' : 'administrador'}`, - updateRole: (email: string, currentRole: string, newRole: string) => + integrationSyncFailed: ({label, errorMessage}: IntegrationSyncFailedParams) => `no se pudo sincronizar con ${label} ("${errorMessage}")`, + addEmployee: ({email, role}: AddEmployeeParams) => `agregó a ${email} como ${role === 'user' ? 'miembro' : 'administrador'}`, + updateRole: ({email, currentRole, newRole}: UpdateRoleParams) => `actualicé el rol ${email} de ${currentRole === 'user' ? 'miembro' : 'administrador'} a ${newRole === 'user' ? 'miembro' : 'administrador'}`, - removeMember: (email: string, role: string) => `eliminado ${role === 'user' ? 'miembro' : 'administrador'} ${email}`, + removeMember: ({email, role}: AddEmployeeParams) => `eliminado ${role === 'user' ? 'miembro' : 'administrador'} ${email}`, }, }, }, @@ -4898,9 +4977,9 @@ export default { allTagLevelsRequired: 'Todas las etiquetas son obligatorias', autoReportedRejectedExpense: ({rejectedBy, rejectReason}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rechazó la solicitud y comentó "${rejectReason}"`, billableExpense: 'La opción facturable ya no es válida', - cashExpenseWithNoReceipt: ({formattedLimit}: ViolationsCashExpenseWithNoReceiptParams) => `Recibo obligatorio para cantidades mayores de ${formattedLimit}`, + cashExpenseWithNoReceipt: ({formattedLimit}: ViolationsCashExpenseWithNoReceiptParams = {}) => `Recibo obligatorio para cantidades mayores de ${formattedLimit}`, categoryOutOfPolicy: 'La categoría ya no es válida', - conversionSurcharge: ({surcharge}: ViolationsConversionSurchargeParams = {}) => `${surcharge}% de recargo aplicado`, + conversionSurcharge: ({surcharge}: ViolationsConversionSurchargeParams) => `${surcharge}% de recargo aplicado`, customUnitOutOfPolicy: 'Tasa inválida para este espacio de trabajo', duplicatedTransaction: 'Duplicado', fieldRequired: 'Los campos del informe son obligatorios', @@ -4909,7 +4988,7 @@ export default { maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Fecha de más de ${maxAge} días`, missingCategory: 'Falta categoría', missingComment: 'Descripción obligatoria para la categoría seleccionada', - missingTag: ({tagName}: ViolationsMissingTagParams) => `Falta ${tagName ?? 'etiqueta'}`, + missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Falta ${tagName ?? 'etiqueta'}`, modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => { switch (type) { case 'distance': @@ -4960,10 +5039,10 @@ export default { return ''; }, smartscanFailed: 'No se pudo escanear el recibo. Introduce los datos manualmente', - someTagLevelsRequired: ({tagName}: ViolationsTagOutOfPolicyParams) => `Falta ${tagName ?? 'Tag'}`, - tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `La etiqueta ${tagName ? `${tagName} ` : ''}ya no es válida`, + someTagLevelsRequired: ({tagName}: ViolationsTagOutOfPolicyParams = {}) => `Falta ${tagName ?? 'Tag'}`, + tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams = {}) => `La etiqueta ${tagName ? `${tagName} ` : ''}ya no es válida`, taxAmountChanged: 'El importe del impuesto fue modificado', - taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName ?? 'El impuesto'} ya no es válido`, + taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams = {}) => `${taxName ?? 'El impuesto'} ya no es válido`, taxRateChanged: 'La tasa de impuesto fue modificada', taxRequired: 'Falta la tasa de impuesto', none: 'Ninguno', @@ -4980,7 +5059,7 @@ export default { hold: 'Bloqueado', }, reportViolations: { - [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} es obligatorio`, + [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} es obligatorio`, }, violationDismissal: { rter: { @@ -5035,12 +5114,12 @@ export default { authenticatePaymentCard: 'Autenticar tarjeta de pago', mobileReducedFunctionalityMessage: 'No puedes hacer cambios en tu suscripción en la aplicación móvil.', badge: { - freeTrial: ({numOfDays}) => `Prueba gratuita: ${numOfDays === 1 ? `queda 1 día` : `quedan ${numOfDays} días`}`, + freeTrial: ({numOfDays}: BadgeFreeTrialParams) => `Prueba gratuita: ${numOfDays === 1 ? `queda 1 día` : `quedan ${numOfDays} días`}`, }, billingBanner: { policyOwnerAmountOwed: { title: 'Tu información de pago está desactualizada', - subtitle: ({date}) => `Actualiza tu tarjeta de pago antes del ${date} para continuar utilizando todas tus herramientas favoritas`, + subtitle: ({date}: BillingBannerSubtitleWithDateParams) => `Actualiza tu tarjeta de pago antes del ${date} para continuar utilizando todas tus herramientas favoritas`, }, policyOwnerAmountOwedOverdue: { title: 'Tu información de pago está desactualizada', @@ -5048,7 +5127,7 @@ export default { }, policyOwnerUnderInvoicing: { title: 'Tu información de pago está desactualizada', - subtitle: ({date}) => `Tu pago está vencido. Por favor, paga tu factura antes del ${date} para evitar la interrupción del servicio.`, + subtitle: ({date}: BillingBannerSubtitleWithDateParams) => `Tu pago está vencido. Por favor, paga tu factura antes del ${date} para evitar la interrupción del servicio.`, }, policyOwnerUnderInvoicingOverdue: { title: 'Tu información de pago está desactualizada', @@ -5056,22 +5135,23 @@ export default { }, billingDisputePending: { title: 'No se ha podido realizar el cobro a tu tarjeta', - subtitle: ({amountOwed, cardEnding}) => + subtitle: ({amountOwed, cardEnding}: BillingBannerDisputePendingParams) => `Has impugnado el cargo ${amountOwed} en la tarjeta terminada en ${cardEnding}. Tu cuenta estará bloqueada hasta que se resuelva la disputa con tu banco.`, }, cardAuthenticationRequired: { title: 'No se ha podido realizar el cobro a tu tarjeta', - subtitle: ({cardEnding}) => + subtitle: ({cardEnding}: BillingBannerCardAuthenticationRequiredParams) => `Tu tarjeta de pago no ha sido autenticada completamente. Por favor, completa el proceso de autenticación para activar tu tarjeta de pago que termina en ${cardEnding}.`, }, insufficientFunds: { title: 'No se ha podido realizar el cobro a tu tarjeta', - subtitle: ({amountOwed}) => + subtitle: ({amountOwed}: BillingBannerInsufficientFundsParams) => `Tu tarjeta de pago fue rechazada por falta de fondos. Vuelve a intentarlo o añade una nueva tarjeta de pago para liquidar tu saldo pendiente de ${amountOwed}.`, }, cardExpired: { title: 'No se ha podido realizar el cobro a tu tarjeta', - subtitle: ({amountOwed}) => `Tu tarjeta de pago ha expirado. Por favor, añade una nueva tarjeta de pago para liquidar tu saldo pendiente de ${amountOwed}.`, + subtitle: ({amountOwed}: BillingBannerCardExpiredParams) => + `Tu tarjeta de pago ha expirado. Por favor, añade una nueva tarjeta de pago para liquidar tu saldo pendiente de ${amountOwed}.`, }, cardExpireSoon: { title: 'Tu tarjeta caducará pronto', @@ -5087,7 +5167,7 @@ export default { subtitle: 'Antes de volver a intentarlo, llama directamente a tu banco para que autorice los cargos de Expensify y elimine las retenciones. De lo contrario, añade una tarjeta de pago diferente.', }, - cardOnDispute: ({amountOwed, cardEnding}) => + cardOnDispute: ({amountOwed, cardEnding}: BillingBannerCardOnDisputeParams) => `Has impugnado el cargo ${amountOwed} en la tarjeta terminada en ${cardEnding}. Tu cuenta estará bloqueada hasta que se resuelva la disputa con tu banco.`, preTrial: { title: 'Iniciar una prueba gratuita', @@ -5096,7 +5176,7 @@ export default { subtitleEnd: 'para que tu equipo pueda empezar a enviar gastos.', }, trialStarted: { - title: ({numOfDays}) => `Prueba gratuita: ¡${numOfDays === 1 ? `queda 1 día` : `quedan ${numOfDays} días`}!`, + title: ({numOfDays}: TrialStartedTitleParams) => `Prueba gratuita: ¡${numOfDays === 1 ? `queda 1 día` : `quedan ${numOfDays} días`}!`, subtitle: 'Añade una tarjeta de pago para seguir utilizando tus funciones favoritas.', }, trialEnded: { @@ -5108,9 +5188,9 @@ export default { title: 'Pago', subtitle: 'Añade una tarjeta para pagar tu suscripción a Expensify.', addCardButton: 'Añade tarjeta de pago', - cardNextPayment: ({nextPaymentDate}) => `Tu próxima fecha de pago es ${nextPaymentDate}.`, - cardEnding: ({cardNumber}) => `Tarjeta terminada en ${cardNumber}`, - cardInfo: ({name, expiration, currency}) => `Nombre: ${name}, Expiración: ${expiration}, Moneda: ${currency}`, + cardNextPayment: ({nextPaymentDate}: CardNextPaymentParams) => `Tu próxima fecha de pago es ${nextPaymentDate}.`, + cardEnding: ({cardNumber}: CardEndingParams) => `Tarjeta terminada en ${cardNumber}`, + cardInfo: ({name, expiration, currency}: CardInfoParams) => `Nombre: ${name}, Expiración: ${expiration}, Moneda: ${currency}`, changeCard: 'Cambiar tarjeta de pago', changeCurrency: 'Cambiar moneda de pago', cardNotFound: 'No se ha añadido ninguna tarjeta de pago', @@ -5129,8 +5209,8 @@ export default { title: 'Tu plan', collect: { title: 'Recolectar', - priceAnnual: ({lower, upper}) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`, - pricePayPerUse: ({lower, upper}) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`, + priceAnnual: ({lower, upper}: YourPlanPriceParams) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`, + pricePayPerUse: ({lower, upper}: YourPlanPriceParams) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`, benefit1: 'SmartScans ilimitados y seguimiento de la distancia', benefit2: 'Tarjetas Expensify con Límites Inteligentes', benefit3: 'Pago de facturas y facturación', @@ -5141,8 +5221,8 @@ export default { }, control: { title: 'Control', - priceAnnual: ({lower, upper}) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`, - pricePayPerUse: ({lower, upper}) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`, + priceAnnual: ({lower, upper}: YourPlanPriceParams) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`, + pricePayPerUse: ({lower, upper}: YourPlanPriceParams) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`, benefit1: 'Todo en Recolectar, más:', benefit2: 'Integraciones con NetSuite y Sage Intacct', benefit3: 'Sincronización de Certinia y Workday', @@ -5173,10 +5253,10 @@ export default { note: 'Nota: Un miembro activo es cualquiera que haya creado, editado, enviado, aprobado, reembolsado, o exportado datos de gastos vinculados al espacio de trabajo de tu empresa.', confirmDetails: 'Confirma los datos de tu nueva suscripción anual:', subscriptionSize: 'Tamaño de suscripción', - activeMembers: ({size}) => `${size} miembros activos/mes`, + activeMembers: ({size}: SubscriptionSizeParams) => `${size} miembros activos/mes`, subscriptionRenews: 'Renovación de la suscripción', youCantDowngrade: 'No puedes bajar de categoría durante tu suscripción anual.', - youAlreadyCommitted: ({size, date}) => + youAlreadyCommitted: ({size, date}: SubscriptionCommitmentParams) => `Ya se ha comprometido a un tamaño de suscripción anual de ${size} miembros activos al mes hasta el ${date}. Puede cambiar a una suscripción de pago por uso en ${date} desactivando la auto-renovación.`, error: { size: 'Por favor ingrese un tamaño de suscripción valido.', @@ -5193,13 +5273,13 @@ export default { title: 'Configuración de suscripción', autoRenew: 'Auto-renovación', autoIncrease: 'Auto-incremento', - saveUpTo: ({amountWithCurrency}) => `Ahorre hasta ${amountWithCurrency} al mes por miembro activo`, + saveUpTo: ({amountWithCurrency}: SubscriptionSettingsSaveUpToParams) => `Ahorre hasta ${amountWithCurrency} al mes por miembro activo`, automaticallyIncrease: 'Aumenta automáticamente tus plazas anuales para dar lugar a los miembros activos que superen el tamaño de tu suscripción. Nota: Esto ampliará la fecha de finalización de tu suscripción anual.', disableAutoRenew: 'Desactivar auto-renovación', helpUsImprove: 'Ayúdanos a mejorar Expensify', whatsMainReason: '¿Cuál es la razón principal por la que deseas desactivar la auto-renovación?', - renewsOn: ({date}) => `Se renovará el ${date}.`, + renewsOn: ({date}: SubscriptionSettingsRenewsOnParams) => `Se renovará el ${date}.`, }, requestEarlyCancellation: { title: 'Solicitar cancelación anticipada', @@ -5248,7 +5328,7 @@ export default { addCopilot: 'Agregar copiloto', membersCanAccessYourAccount: 'Estos miembros pueden acceder a tu cuenta:', youCanAccessTheseAccounts: 'Puedes acceder a estas cuentas a través del conmutador de cuentas:', - role: (role?: string): string => { + role: ({role}: OptionalParam = {}) => { switch (role) { case CONST.DELEGATE_ROLE.ALL: return 'Completo'; @@ -5259,10 +5339,11 @@ export default { } }, genericError: '¡Ups! Ha ocurrido un error. Por favor, inténtalo de nuevo.', + onBehalfOfMessage: ({delegator}: DelegatorParams) => `en nombre de ${delegator}`, accessLevel: 'Nivel de acceso', confirmCopilot: 'Confirma tu copiloto a continuación.', accessLevelDescription: 'Elige un nivel de acceso a continuación. Tanto el acceso Completo como el Limitado permiten a los copilotos ver todas las conversaciones y gastos.', - roleDescription: (role?: string): string => { + roleDescription: ({role}: OptionalParam = {}) => { switch (role) { case CONST.DELEGATE_ROLE.ALL: return 'Permite a otro miembro realizar todas las acciones en tu cuenta, en tu nombre. Incluye chat, presentaciones, aprobaciones, pagos, actualizaciones de configuración y más.'; @@ -5288,9 +5369,9 @@ export default { nothingToPreview: 'Nada que previsualizar', editJson: 'Editar JSON:', preview: 'Previa:', - missingProperty: ({propertyName}) => `Falta ${propertyName}`, - invalidProperty: ({propertyName, expectedType}) => `Propiedad inválida: ${propertyName} - Esperado: ${expectedType}`, - invalidValue: ({expectedValues}) => `Valor inválido - Esperado: ${expectedValues}`, + missingProperty: ({propertyName}: MissingPropertyParams) => `Falta ${propertyName}`, + invalidProperty: ({propertyName, expectedType}: InvalidPropertyParams) => `Propiedad inválida: ${propertyName} - Esperado: ${expectedType}`, + invalidValue: ({expectedValues}: InvalidValueParams) => `Valor inválido - Esperado: ${expectedValues}`, missingValue: 'Valor en falta', createReportAction: 'Crear Report Action', reportAction: 'Report Action', @@ -5305,4 +5386,6 @@ export default { time: 'Hora', none: 'Ninguno', }, -} satisfies EnglishTranslation; +}; + +export default translations satisfies TranslationDeepObject; diff --git a/src/languages/params.ts b/src/languages/params.ts new file mode 100644 index 000000000000..8ed122283064 --- /dev/null +++ b/src/languages/params.ts @@ -0,0 +1,754 @@ +import type {OnyxInputOrEntry, ReportAction} from '@src/types/onyx'; +import type {DelegateRole} from '@src/types/onyx/Account'; +import type {ConnectionName, PolicyConnectionSyncStage, SageIntacctMappingName, Unit} from '@src/types/onyx/Policy'; +import type {ViolationDataType} from '@src/types/onyx/TransactionViolation'; + +type AddressLineParams = { + lineNumber: number; +}; + +type CharacterLimitParams = { + limit: number | string; +}; + +type AssigneeParams = { + assignee: string; +}; + +type CharacterLengthLimitParams = { + limit: number; + length: number; +}; + +type ZipCodeExampleFormatParams = { + zipSampleFormat: string; +}; + +type LoggedInAsParams = { + email: string; +}; + +type SignUpNewFaceCodeParams = { + login: string; +}; + +type WelcomeEnterMagicCodeParams = { + login: string; +}; + +type AlreadySignedInParams = { + email: string; +}; + +type GoBackMessageParams = { + provider: string; +}; + +type LocalTimeParams = { + user: string; + time: string; +}; + +type EditActionParams = { + action: OnyxInputOrEntry; +}; + +type DeleteActionParams = { + action: OnyxInputOrEntry; +}; + +type DeleteConfirmationParams = { + action: OnyxInputOrEntry; +}; + +type BeginningOfChatHistoryDomainRoomPartOneParams = { + domainRoom: string; +}; + +type BeginningOfChatHistoryAdminRoomPartOneParams = { + workspaceName: string; +}; + +type BeginningOfChatHistoryAnnounceRoomPartOneParams = { + workspaceName: string; +}; + +type BeginningOfChatHistoryAnnounceRoomPartTwo = { + workspaceName: string; +}; + +type WelcomeToRoomParams = { + roomName: string; +}; + +type UsePlusButtonParams = { + additionalText: string; +}; + +type ReportArchiveReasonsClosedParams = { + displayName: string; +}; + +type ReportArchiveReasonsMergedParams = { + displayName: string; + oldDisplayName: string; +}; + +type ReportArchiveReasonsRemovedFromPolicyParams = { + displayName: string; + policyName: string; + shouldUseYou?: boolean; +}; + +type ReportPolicyNameParams = { + policyName: string; +}; + +type ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams = { + policyName: string; +}; + +type RequestCountParams = { + count: number; + scanningReceipts: number; + pendingReceipts: number; +}; + +type SettleExpensifyCardParams = { + formattedAmount: string; +}; + +type RequestAmountParams = {amount: string}; + +type RequestedAmountMessageParams = {formattedAmount: string; comment?: string}; + +type SplitAmountParams = {amount: string}; + +type DidSplitAmountMessageParams = {formattedAmount: string; comment: string}; + +type UserSplitParams = {amount: string}; + +type PayerOwesAmountParams = {payer: string; amount: number | string; comment?: string}; + +type PayerOwesParams = {payer: string}; + +type CompanyCardFeedNameParams = {feedName: string}; + +type PayerPaidAmountParams = {payer?: string; amount: number | string}; + +type ApprovedAmountParams = {amount: number | string}; + +type ForwardedAmountParams = {amount: number | string}; + +type ManagerApprovedParams = {manager: string}; + +type ManagerApprovedAmountParams = {manager: string; amount: number | string}; + +type PayerPaidParams = {payer: string}; + +type PayerSettledParams = {amount: number | string}; + +type WaitingOnBankAccountParams = {submitterDisplayName: string}; + +type CanceledRequestParams = {amount: string; submitterDisplayName: string}; + +type AdminCanceledRequestParams = {manager: string; amount: string}; + +type SettledAfterAddedBankAccountParams = {submitterDisplayName: string; amount: string}; + +type PaidElsewhereWithAmountParams = {payer?: string; amount: string}; + +type PaidWithExpensifyWithAmountParams = {payer?: string; amount: string}; + +type ThreadRequestReportNameParams = {formattedAmount: string; comment: string}; + +type ThreadSentMoneyReportNameParams = {formattedAmount: string; comment: string}; + +type SizeExceededParams = {maxUploadSizeInMB: number}; + +type ResolutionConstraintsParams = {minHeightInPx: number; minWidthInPx: number; maxHeightInPx: number; maxWidthInPx: number}; + +type NotAllowedExtensionParams = {allowedExtensions: string[]}; + +type EnterMagicCodeParams = {contactMethod: string}; + +type TransferParams = {amount: string}; + +type InstantSummaryParams = {rate: string; minAmount: string}; + +type NotYouParams = {user: string}; + +type DateShouldBeBeforeParams = {dateString: string}; + +type DateShouldBeAfterParams = {dateString: string}; + +type WeSentYouMagicSignInLinkParams = {login: string; loginType: string}; + +type ToValidateLoginParams = {primaryLogin: string; secondaryLogin: string}; + +type NoLongerHaveAccessParams = {primaryLogin: string}; + +type OurEmailProviderParams = {login: string}; + +type ConfirmThatParams = {login: string}; + +type UntilTimeParams = {time: string}; + +type StepCounterParams = {step: number; total?: number; text?: string}; + +type UserIsAlreadyMemberParams = {login: string; name: string}; + +type GoToRoomParams = {roomName: string}; + +type WelcomeNoteParams = {workspaceName: string}; + +type RoomNameReservedErrorParams = {reservedName: string}; + +type RenamedRoomActionParams = {oldName: string; newName: string}; + +type RoomRenamedToParams = {newName: string}; + +type OOOEventSummaryFullDayParams = {summary: string; dayCount: number; date: string}; + +type OOOEventSummaryPartialDayParams = {summary: string; timePeriod: string; date: string}; + +type ParentNavigationSummaryParams = {reportName?: string; workspaceName?: string}; + +type SetTheRequestParams = {valueName: string; newValueToDisplay: string}; + +type SetTheDistanceMerchantParams = {translatedChangedField: string; newMerchant: string; newAmountToDisplay: string}; + +type RemovedTheRequestParams = {valueName: string; oldValueToDisplay: string}; + +type UpdatedTheRequestParams = {valueName: string; newValueToDisplay: string; oldValueToDisplay: string}; + +type UpdatedTheDistanceMerchantParams = {translatedChangedField: string; newMerchant: string; oldMerchant: string; newAmountToDisplay: string; oldAmountToDisplay: string}; + +type FormattedMaxLengthParams = {formattedMaxLength: string}; + +type WalletProgramParams = {walletProgram: string}; + +type ViolationsAutoReportedRejectedExpenseParams = {rejectedBy: string; rejectReason: string}; + +type ViolationsCashExpenseWithNoReceiptParams = {formattedLimit?: string} | undefined; + +type ViolationsConversionSurchargeParams = {surcharge: number}; + +type ViolationsInvoiceMarkupParams = {invoiceMarkup: number}; + +type ViolationsMaxAgeParams = {maxAge: number}; + +type ViolationsMissingTagParams = {tagName?: string} | undefined; + +type ViolationsModifiedAmountParams = {type?: ViolationDataType; displayPercentVariance?: number}; + +type ViolationsOverAutoApprovalLimitParams = {formattedLimit: string}; + +type ViolationsOverCategoryLimitParams = {formattedLimit: string}; + +type ViolationsOverLimitParams = {formattedLimit: string}; + +type ViolationsPerDayLimitParams = {formattedLimit: string}; + +type ViolationsReceiptRequiredParams = {formattedLimit?: string; category?: string}; + +type ViolationsRterParams = { + brokenBankConnection: boolean; + isAdmin: boolean; + email?: string; + isTransactionOlderThan7Days: boolean; + member?: string; +}; + +type ViolationsTagOutOfPolicyParams = {tagName?: string} | undefined; + +type ViolationsTaxOutOfPolicyParams = {taxName?: string} | undefined; + +type PaySomeoneParams = {name?: string} | undefined; + +type TaskCreatedActionParams = {title: string}; + +type OptionalParam = Partial; + +type TermsParams = {amount: string}; + +type ElectronicFundsParams = {percentage: string; amount: string}; + +type LogSizeParams = {size: number}; + +type LogSizeAndDateParams = {size: number; date: string}; + +type HeldRequestParams = {comment: string}; + +type DistanceRateOperationsParams = {count: number}; + +type ReimbursementRateParams = {unit: Unit}; + +type ConfirmHoldExpenseParams = {transactionCount: number}; + +type ChangeFieldParams = {oldValue?: string; newValue: string; fieldName: string}; + +type ChangePolicyParams = {fromPolicy: string; toPolicy: string}; + +type ChangeTypeParams = {oldType: string; newType: string}; + +type DelegateSubmitParams = {delegateUser: string; originalManager: string}; + +type AccountOwnerParams = {accountOwnerEmail: string}; + +type ExportedToIntegrationParams = {label: string; markedManually?: boolean; inProgress?: boolean; lastModified?: string}; + +type IntegrationsMessageParams = { + label: string; + result: { + code?: number; + messages?: string[]; + title?: string; + link?: { + url: string; + text: string; + }; + }; +}; + +type MarkedReimbursedParams = {amount: string; currency: string}; + +type MarkReimbursedFromIntegrationParams = {amount: string; currency: string}; + +type ShareParams = {to: string}; + +type UnshareParams = {to: string}; + +type StripePaidParams = {amount: string; currency: string}; + +type UnapprovedParams = {amount: string; currency: string}; + +type RemoveMembersWarningPrompt = { + memberName: string; + ownerName: string; +}; + +type RemoveMemberPromptParams = { + memberName: string; +}; + +type DeleteExpenseTranslationParams = { + count: number; +}; + +type IssueVirtualCardParams = { + assignee: string; + link: string; +}; + +type ApprovalWorkflowErrorParams = { + name1: string; + name2: string; +}; + +type ConnectionNameParams = { + connectionName: ConnectionName; +}; + +type LastSyncDateParams = { + connectionName: string; + formattedDate: string; +}; + +type CustomersOrJobsLabelParams = { + importFields: string[]; + importType: string; +}; + +type ExportAgainModalDescriptionParams = { + reportName: string; + connectionName: ConnectionName; +}; + +type IntegrationSyncFailedParams = {label: string; errorMessage: string}; + +type AddEmployeeParams = {email: string; role: string}; + +type UpdateRoleParams = {email: string; currentRole: string; newRole: string}; + +type RemoveMemberParams = {email: string; role: string}; + +type DateParams = {date: string}; + +type FiltersAmountBetweenParams = {greaterThan: string; lessThan: string}; + +type StatementPageTitleParams = {year: string | number; monthName: string}; + +type DisconnectPromptParams = {currentIntegration?: ConnectionName} | undefined; + +type DisconnectTitleParams = {integration?: ConnectionName} | undefined; + +type AmountWithCurrencyParams = {amountWithCurrency: string}; + +type SelectedNumberParams = {selectedNumber: number}; + +type LowerUpperParams = {lower: string; upper: string}; + +type CategoryNameParams = {categoryName: string}; + +type TaxAmountParams = {taxAmount: number}; + +type SecondaryLoginParams = {secondaryLogin: string}; + +type OwnerOwesAmountParams = {amount: string; email: string}; + +type ChangeOwnerSubscriptionParams = {usersCount: number; finalCount: number}; + +type ChangeOwnerDuplicateSubscriptionParams = {email: string; workspaceName: string}; + +type ChangeOwnerHasFailedSettlementsParams = {email: string}; + +type ActionsAreCurrentlyRestricted = {workspaceName: string}; + +type WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams = {workspaceOwnerName: string}; + +type RenamedWorkspaceNameActionParams = {oldName: string; newName: string}; + +type StatementTitleParams = {year: number | string; monthName: string}; + +type BadgeFreeTrialParams = {numOfDays: number}; + +type BillingBannerSubtitleWithDateParams = {date: string}; + +type BillingBannerDisputePendingParams = {amountOwed: number; cardEnding: string}; + +type BillingBannerCardAuthenticationRequiredParams = {cardEnding: string}; + +type BillingBannerInsufficientFundsParams = {amountOwed: number}; + +type BillingBannerCardExpiredParams = {amountOwed: number}; + +type BillingBannerCardOnDisputeParams = {amountOwed: string; cardEnding: string}; + +type TrialStartedTitleParams = {numOfDays: number}; + +type CardNextPaymentParams = {nextPaymentDate: string}; + +type CardEndingParams = {cardNumber: string}; + +type CardInfoParams = {name: string; expiration: string; currency: string}; + +type YourPlanPriceParams = {lower: string; upper: string}; + +type SubscriptionSizeParams = {size: number}; + +type SubscriptionCommitmentParams = {size: number; date: string}; + +type SubscriptionSettingsSaveUpToParams = {amountWithCurrency: string}; + +type SubscriptionSettingsRenewsOnParams = {date: string}; + +type UnapproveWithIntegrationWarningParams = {accountingIntegration: string}; + +type IncorrectZipFormatParams = {zipFormat?: string} | undefined; + +type ExportIntegrationSelectedParams = {connectionName: ConnectionName}; + +type DefaultVendorDescriptionParams = {isReimbursable: boolean}; + +type RequiredFieldParams = {fieldName: string}; + +type ImportFieldParams = {importField: string}; + +type DimensionsCountParams = {dimensionsCount: number}; + +type IntacctMappingTitleParams = {mappingName: SageIntacctMappingName}; + +type AgeParams = {age: number}; + +type LastSyncAccountingParams = {relativeDate: string}; + +type SyncStageNameConnectionsParams = {stage: PolicyConnectionSyncStage}; + +type ReconciliationWorksParams = {lastFourPAN: string}; + +type DelegateRoleParams = {role: DelegateRole}; + +type DelegatorParams = {delegator: string}; + +type RoleNamesParams = {role: string}; + +type AssignCardParams = { + assignee: string; + feed: string; +}; + +type SpreadSheetColumnParams = { + name: string; +}; + +type SpreadFieldNameParams = { + fieldName: string; +}; + +type SpreadCategoriesParams = { + categories: number; +}; + +type AssignedYouCardParams = { + assigner: string; +}; + +type FeatureNameParams = { + featureName: string; +}; + +type AutoPayApprovedReportsLimitErrorParams = { + currency?: string; +}; + +type DefaultAmountParams = { + defaultAmount: string; +}; + +type RemovedFromApprovalWorkflowParams = { + submittersNames: string[]; +}; + +type IntegrationExportParams = { + integration: string; + type?: string; +}; + +type ConnectionParams = { + connection: string; +}; + +type MissingPropertyParams = { + propertyName: string; +}; + +type InvalidPropertyParams = { + propertyName: string; + expectedType: string; +}; + +type InvalidValueParams = { + expectedValues: string; +}; + +type ImportTagsSuccessfullDescriptionParams = { + tags: number; +}; + +type ImportedTagsMessageParams = { + columnCounts: number; +}; + +type ImportMembersSuccessfullDescriptionParams = { + members: number; +}; + +type AuthenticationErrorParams = { + connectionName: string; +}; + +export type { + AuthenticationErrorParams, + ImportMembersSuccessfullDescriptionParams, + ImportedTagsMessageParams, + ImportTagsSuccessfullDescriptionParams, + MissingPropertyParams, + InvalidPropertyParams, + InvalidValueParams, + ConnectionParams, + IntegrationExportParams, + RemovedFromApprovalWorkflowParams, + DefaultAmountParams, + AutoPayApprovedReportsLimitErrorParams, + FeatureNameParams, + SpreadSheetColumnParams, + SpreadFieldNameParams, + AssignedYouCardParams, + SpreadCategoriesParams, + DelegateRoleParams, + DelegatorParams, + ReconciliationWorksParams, + LastSyncAccountingParams, + SyncStageNameConnectionsParams, + AgeParams, + RequiredFieldParams, + DimensionsCountParams, + IntacctMappingTitleParams, + ImportFieldParams, + AssigneeParams, + DefaultVendorDescriptionParams, + ExportIntegrationSelectedParams, + UnapproveWithIntegrationWarningParams, + IncorrectZipFormatParams, + CardNextPaymentParams, + CardEndingParams, + CardInfoParams, + YourPlanPriceParams, + SubscriptionSizeParams, + SubscriptionCommitmentParams, + SubscriptionSettingsSaveUpToParams, + SubscriptionSettingsRenewsOnParams, + BadgeFreeTrialParams, + BillingBannerSubtitleWithDateParams, + BillingBannerDisputePendingParams, + BillingBannerCardAuthenticationRequiredParams, + BillingBannerInsufficientFundsParams, + BillingBannerCardExpiredParams, + BillingBannerCardOnDisputeParams, + TrialStartedTitleParams, + RemoveMemberPromptParams, + StatementTitleParams, + RenamedWorkspaceNameActionParams, + WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams, + ActionsAreCurrentlyRestricted, + ChangeOwnerHasFailedSettlementsParams, + OwnerOwesAmountParams, + ChangeOwnerDuplicateSubscriptionParams, + ChangeOwnerSubscriptionParams, + SecondaryLoginParams, + TaxAmountParams, + CategoryNameParams, + SelectedNumberParams, + AmountWithCurrencyParams, + LowerUpperParams, + LogSizeAndDateParams, + AddressLineParams, + AdminCanceledRequestParams, + AlreadySignedInParams, + ApprovedAmountParams, + BeginningOfChatHistoryAdminRoomPartOneParams, + BeginningOfChatHistoryAnnounceRoomPartOneParams, + BeginningOfChatHistoryAnnounceRoomPartTwo, + BeginningOfChatHistoryDomainRoomPartOneParams, + CanceledRequestParams, + CharacterLimitParams, + ConfirmHoldExpenseParams, + ConfirmThatParams, + CompanyCardFeedNameParams, + DateShouldBeAfterParams, + DateShouldBeBeforeParams, + DeleteActionParams, + DeleteConfirmationParams, + DidSplitAmountMessageParams, + DistanceRateOperationsParams, + EditActionParams, + ElectronicFundsParams, + EnterMagicCodeParams, + FormattedMaxLengthParams, + ForwardedAmountParams, + GoBackMessageParams, + GoToRoomParams, + HeldRequestParams, + InstantSummaryParams, + IssueVirtualCardParams, + LocalTimeParams, + LogSizeParams, + LoggedInAsParams, + ManagerApprovedAmountParams, + ManagerApprovedParams, + SignUpNewFaceCodeParams, + NoLongerHaveAccessParams, + NotAllowedExtensionParams, + NotYouParams, + OOOEventSummaryFullDayParams, + OOOEventSummaryPartialDayParams, + OurEmailProviderParams, + PaidElsewhereWithAmountParams, + PaidWithExpensifyWithAmountParams, + ParentNavigationSummaryParams, + PaySomeoneParams, + PayerOwesAmountParams, + PayerOwesParams, + RoleNamesParams, + PayerPaidAmountParams, + PayerPaidParams, + PayerSettledParams, + ReimbursementRateParams, + RemovedTheRequestParams, + RenamedRoomActionParams, + ReportArchiveReasonsClosedParams, + ReportArchiveReasonsMergedParams, + ReportPolicyNameParams, + ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams, + ReportArchiveReasonsRemovedFromPolicyParams, + RequestAmountParams, + RequestCountParams, + RequestedAmountMessageParams, + ResolutionConstraintsParams, + RoomNameReservedErrorParams, + RoomRenamedToParams, + SetTheDistanceMerchantParams, + SetTheRequestParams, + SettleExpensifyCardParams, + SettledAfterAddedBankAccountParams, + SizeExceededParams, + SplitAmountParams, + StepCounterParams, + TaskCreatedActionParams, + TermsParams, + ThreadRequestReportNameParams, + ThreadSentMoneyReportNameParams, + ToValidateLoginParams, + TransferParams, + UntilTimeParams, + UpdatedTheDistanceMerchantParams, + UpdatedTheRequestParams, + UsePlusButtonParams, + UserIsAlreadyMemberParams, + UserSplitParams, + ViolationsAutoReportedRejectedExpenseParams, + ViolationsCashExpenseWithNoReceiptParams, + ViolationsConversionSurchargeParams, + ViolationsInvoiceMarkupParams, + ViolationsMaxAgeParams, + ViolationsMissingTagParams, + ViolationsModifiedAmountParams, + ViolationsOverAutoApprovalLimitParams, + ViolationsOverCategoryLimitParams, + ViolationsOverLimitParams, + ViolationsPerDayLimitParams, + ViolationsReceiptRequiredParams, + ViolationsRterParams, + ViolationsTagOutOfPolicyParams, + ViolationsTaxOutOfPolicyParams, + WaitingOnBankAccountParams, + WalletProgramParams, + WeSentYouMagicSignInLinkParams, + WelcomeEnterMagicCodeParams, + WelcomeNoteParams, + WelcomeToRoomParams, + ZipCodeExampleFormatParams, + ChangeFieldParams, + ChangePolicyParams, + ChangeTypeParams, + ExportedToIntegrationParams, + DelegateSubmitParams, + AccountOwnerParams, + IntegrationsMessageParams, + MarkedReimbursedParams, + MarkReimbursedFromIntegrationParams, + ShareParams, + UnshareParams, + StripePaidParams, + UnapprovedParams, + RemoveMembersWarningPrompt, + DeleteExpenseTranslationParams, + ApprovalWorkflowErrorParams, + ConnectionNameParams, + LastSyncDateParams, + CustomersOrJobsLabelParams, + ExportAgainModalDescriptionParams, + IntegrationSyncFailedParams, + AddEmployeeParams, + UpdateRoleParams, + RemoveMemberParams, + DateParams, + FiltersAmountBetweenParams, + StatementPageTitleParams, + DisconnectPromptParams, + DisconnectTitleParams, + CharacterLengthLimitParams, + OptionalParam, + AssignCardParams, +}; diff --git a/src/languages/translations.ts b/src/languages/translations.ts index 4d89f1f529de..ec99d999f94e 100644 --- a/src/languages/translations.ts +++ b/src/languages/translations.ts @@ -1,7 +1,7 @@ import en from './en'; import es from './es'; import esES from './es-ES'; -import type {TranslationBase, TranslationFlatObject} from './types'; +import type {FlatTranslationsObject, TranslationDeepObject} from './types'; /** * Converts an object to it's flattened version. @@ -12,10 +12,10 @@ import type {TranslationBase, TranslationFlatObject} from './types'; */ // Necessary to export so that it is accessible to the unit tests // eslint-disable-next-line rulesdir/no-inline-named-export -export function flattenObject(obj: TranslationBase): TranslationFlatObject { +export function flattenObject(obj: TranslationDeepObject): FlatTranslationsObject { const result: Record = {}; - const recursive = (data: TranslationBase, key: string): void => { + const recursive = (data: TranslationDeepObject, key: string): void => { // If the data is a function or not a object (eg. a string or array), // it's the final value for the key being built and there is no need // for more recursion @@ -27,7 +27,7 @@ export function flattenObject(obj: TranslationBase): TranslationFlatObject { // Recursive call to the keys and connect to the respective data Object.keys(data).forEach((k) => { isEmpty = false; - recursive(data[k] as TranslationBase, key ? `${key}.${k}` : k); + recursive(data[k] as TranslationDeepObject, key ? `${key}.${k}` : k); }); // Check for when the object is empty but a key exists, so that @@ -39,7 +39,7 @@ export function flattenObject(obj: TranslationBase): TranslationFlatObject { }; recursive(obj, ''); - return result as TranslationFlatObject; + return result as FlatTranslationsObject; } export default { diff --git a/src/languages/types.ts b/src/languages/types.ts index a7a11fafb27b..0bdf740d982e 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -1,278 +1,53 @@ -import type {OnyxInputOrEntry, ReportAction} from '@src/types/onyx'; -import type {Unit} from '@src/types/onyx/Policy'; -import type {ViolationDataType} from '@src/types/onyx/TransactionViolation'; +/* eslint-disable @typescript-eslint/no-explicit-any */ import type en from './en'; -type AddressLineParams = { - lineNumber: number; -}; - -type CharacterLimitParams = { - limit: number; -}; - -type ZipCodeExampleFormatParams = { - zipSampleFormat: string; -}; - -type LoggedInAsParams = { - email: string; -}; - -type SignUpNewFaceCodeParams = { - login: string; -}; - -type WelcomeEnterMagicCodeParams = { - login: string; -}; - -type AlreadySignedInParams = { - email: string; -}; - -type GoBackMessageParams = { - provider: string; -}; - -type LocalTimeParams = { - user: string; - time: string; -}; - -type EditActionParams = { - action: OnyxInputOrEntry; -}; - -type DeleteActionParams = { - action: OnyxInputOrEntry; -}; - -type DeleteConfirmationParams = { - action: OnyxInputOrEntry; -}; - -type BeginningOfChatHistoryDomainRoomPartOneParams = { - domainRoom: string; -}; - -type BeginningOfChatHistoryAdminRoomPartOneParams = { - workspaceName: string; -}; - -type BeginningOfChatHistoryAnnounceRoomPartOneParams = { - workspaceName: string; -}; - -type BeginningOfChatHistoryAnnounceRoomPartTwo = { - workspaceName: string; -}; - -type WelcomeToRoomParams = { - roomName: string; -}; - -type UsePlusButtonParams = { - additionalText: string; -}; - -type ReportArchiveReasonsClosedParams = { - displayName: string; -}; - -type ReportArchiveReasonsMergedParams = { - displayName: string; - oldDisplayName: string; -}; - -type ReportArchiveReasonsRemovedFromPolicyParams = { - displayName: string; - policyName: string; - shouldUseYou?: boolean; -}; - -type ReportArchiveReasonsPolicyDeletedParams = { - policyName: string; -}; - -type ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams = { - policyName: string; -}; - -type RequestCountParams = { - count: number; - scanningReceipts: number; - pendingReceipts: number; -}; - -type SettleExpensifyCardParams = { - formattedAmount: string; -}; - -type RequestAmountParams = {amount: string}; - -type RequestedAmountMessageParams = {formattedAmount: string; comment?: string}; - -type SplitAmountParams = {amount: string}; - -type DidSplitAmountMessageParams = {formattedAmount: string; comment: string}; - -type UserSplitParams = {amount: string}; - -type PayerOwesAmountParams = {payer: string; amount: number | string; comment?: string}; - -type PayerOwesParams = {payer: string}; - -type CompanyCardFeedNameParams = {feedName: string}; - -type PayerPaidAmountParams = {payer?: string; amount: number | string}; - -type ApprovedAmountParams = {amount: number | string}; - -type ForwardedAmountParams = {amount: number | string}; - -type ManagerApprovedParams = {manager: string}; - -type ManagerApprovedAmountParams = {manager: string; amount: number | string}; - -type PayerPaidParams = {payer: string}; - -type PayerSettledParams = {amount: number | string}; - -type WaitingOnBankAccountParams = {submitterDisplayName: string}; - -type CanceledRequestParams = {amount: string; submitterDisplayName: string}; - -type AdminCanceledRequestParams = {manager: string; amount: string}; - -type SettledAfterAddedBankAccountParams = {submitterDisplayName: string; amount: string}; - -type PaidElsewhereWithAmountParams = {payer?: string; amount: string}; - -type PaidWithExpensifyWithAmountParams = {payer?: string; amount: string}; - -type ThreadRequestReportNameParams = {formattedAmount: string; comment: string}; - -type ThreadSentMoneyReportNameParams = {formattedAmount: string; comment: string}; - -type SizeExceededParams = {maxUploadSizeInMB: number}; - -type ResolutionConstraintsParams = {minHeightInPx: number; minWidthInPx: number; maxHeightInPx: number; maxWidthInPx: number}; - -type NotAllowedExtensionParams = {allowedExtensions: string[]}; - -type EnterMagicCodeParams = {contactMethod: string}; - -type TransferParams = {amount: string}; - -type InstantSummaryParams = {rate: string; minAmount: string}; - -type NotYouParams = {user: string}; - -type DateShouldBeBeforeParams = {dateString: string}; - -type DateShouldBeAfterParams = {dateString: string}; - -type WeSentYouMagicSignInLinkParams = {login: string; loginType: string}; - -type ToValidateLoginParams = {primaryLogin: string; secondaryLogin: string}; - -type NoLongerHaveAccessParams = {primaryLogin: string}; - -type OurEmailProviderParams = {login: string}; - -type ConfirmThatParams = {login: string}; - -type UntilTimeParams = {time: string}; - -type StepCounterParams = {step: number; total?: number; text?: string}; - -type UserIsAlreadyMemberParams = {login: string; name: string}; - -type GoToRoomParams = {roomName: string}; - -type WelcomeNoteParams = {workspaceName: string}; - -type RoomNameReservedErrorParams = {reservedName: string}; - -type RenamedRoomActionParams = {oldName: string; newName: string}; - -type RoomRenamedToParams = {newName: string}; - -type OOOEventSummaryFullDayParams = {summary: string; dayCount: number; date: string}; - -type OOOEventSummaryPartialDayParams = {summary: string; timePeriod: string; date: string}; - -type ParentNavigationSummaryParams = {reportName?: string; workspaceName?: string}; - -type SetTheRequestParams = {valueName: string; newValueToDisplay: string}; - -type SetTheDistanceMerchantParams = {translatedChangedField: string; newMerchant: string; newAmountToDisplay: string}; - -type RemovedTheRequestParams = {valueName: string; oldValueToDisplay: string}; - -type UpdatedTheRequestParams = {valueName: string; newValueToDisplay: string; oldValueToDisplay: string}; - -type UpdatedTheDistanceMerchantParams = {translatedChangedField: string; newMerchant: string; oldMerchant: string; newAmountToDisplay: string; oldAmountToDisplay: string}; - -type FormattedMaxLengthParams = {formattedMaxLength: string}; - -type WalletProgramParams = {walletProgram: string}; - -type ViolationsAutoReportedRejectedExpenseParams = {rejectedBy: string; rejectReason: string}; - -type ViolationsCashExpenseWithNoReceiptParams = {formattedLimit?: string}; - -type ViolationsConversionSurchargeParams = {surcharge?: number}; - -type ViolationsInvoiceMarkupParams = {invoiceMarkup?: number}; - -type ViolationsMaxAgeParams = {maxAge: number}; - -type ViolationsMissingTagParams = {tagName?: string}; - -type ViolationsModifiedAmountParams = {type?: ViolationDataType; displayPercentVariance?: number}; - -type ViolationsOverAutoApprovalLimitParams = {formattedLimit?: string}; - -type ViolationsOverCategoryLimitParams = {formattedLimit?: string}; - -type ViolationsOverLimitParams = {formattedLimit?: string}; - -type ViolationsPerDayLimitParams = {formattedLimit?: string}; - -type ViolationsReceiptRequiredParams = {formattedLimit?: string; category?: string}; - -type ViolationsRterParams = { - brokenBankConnection: boolean; - isAdmin: boolean; - email?: string; - isTransactionOlderThan7Days: boolean; - member?: string; -}; - -type ViolationsTagOutOfPolicyParams = {tagName?: string}; - -type ViolationsTaxOutOfPolicyParams = {taxName?: string}; - -type PaySomeoneParams = {name?: string}; - -type TaskCreatedActionParams = {title: string}; - -/* Translation Object types */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type TranslationBaseValue = string | string[] | ((...args: any[]) => string); - -type TranslationBase = {[key: string]: TranslationBaseValue | TranslationBase}; - -/* Flat Translation Object types */ -// Flattens an object and returns concatenations of all the keys of nested objects -type FlattenObject = { +type PluralParams = {count: number}; +type PluralHandler = ((count: number) => string) | string; +type PluralForm = { + zero?: string; + one: string; + two?: string; + few?: PluralHandler; + many?: PluralHandler; + other: PluralHandler; +}; + +/** + * Retrieves the first argument of a function + */ +type FirstArgument = TFunction extends (arg: infer A, ...args: any[]) => any ? A : never; + +/** + * Translation value can be a string or a function that returns a string + */ +type TranslationLeafValue = TStringOrFunction extends string + ? string + : ( + arg: FirstArgument extends Record | undefined ? FirstArgument : Record, + ...noOtherArguments: unknown[] + ) => string | PluralForm; + +/** + * Translation object is a recursive object that can contain other objects or string/function values + */ +type TranslationDeepObject = { // eslint-disable-next-line @typescript-eslint/no-explicit-any - [TKey in keyof TObject]: TObject[TKey] extends (...args: any[]) => any - ? `${TPrefix}${TKey & string}` - : // eslint-disable-next-line @typescript-eslint/no-explicit-any - TObject[TKey] extends any[] + [Path in keyof TTranslations]: TTranslations[Path] extends string | ((...args: any[]) => any) + ? TranslationLeafValue + : TTranslations[Path] extends number | boolean | null | undefined | unknown[] + ? string + : TranslationDeepObject; +}; + +/** + * Flattens an object and returns concatenations of all the keys of nested objects + * + * Ex: + * Input: { common: { yes: "Yes", no: "No" }} + * Output: "common.yes" | "common.no" + */ +type FlattenObject = { + [TKey in keyof TObject]: TObject[TKey] extends (arg: any) => any ? `${TPrefix}${TKey & string}` : // eslint-disable-next-line @typescript-eslint/ban-types TObject[TKey] extends object @@ -280,222 +55,43 @@ type FlattenObject = { : `${TPrefix}${TKey & string}`; }[keyof TObject]; -// Retrieves a type for a given key path (calculated from the type above) -type TranslateType = TPath extends keyof TObject - ? TObject[TPath] - : TPath extends `${infer TKey}.${infer TRest}` - ? TKey extends keyof TObject - ? TranslateType +/** + * Retrieves a type for a given key path (calculated from the type above) + */ +type TranslationValue = TKey extends keyof TObject + ? TObject[TKey] + : TKey extends `${infer TPathKey}.${infer TRest}` + ? TPathKey extends keyof TObject + ? TranslationValue : never : never; -type EnglishTranslation = typeof en; - -type TranslationPaths = FlattenObject; - -type TranslationFlatObject = { - [TKey in TranslationPaths]: TranslateType; -}; - -type TermsParams = {amount: string}; - -type ElectronicFundsParams = {percentage: string; amount: string}; - -type LogSizeParams = {size: number}; - -type HeldRequestParams = {comment: string}; - -type DistanceRateOperationsParams = {count: number}; - -type ReimbursementRateParams = {unit: Unit}; - -type ConfirmHoldExpenseParams = {transactionCount: number}; - -type ChangeFieldParams = {oldValue?: string; newValue: string; fieldName: string}; - -type ChangePolicyParams = {fromPolicy: string; toPolicy: string}; - -type ChangeTypeParams = {oldType: string; newType: string}; - -type DelegateSubmitParams = {delegateUser: string; originalManager: string}; - -type AccountOwnerParams = {accountOwnerEmail: string}; - -type ExportedToIntegrationParams = {label: string; markedManually?: boolean; inProgress?: boolean; lastModified?: string}; - -type IntegrationsMessageParams = { - label: string; - result: { - code?: number; - messages?: string[]; - title?: string; - link?: { - url: string; - text: string; - }; - }; -}; - -type MarkedReimbursedParams = {amount: string; currency: string}; - -type MarkReimbursedFromIntegrationParams = {amount: string; currency: string}; - -type ShareParams = {to: string}; - -type UnshareParams = {to: string}; - -type StripePaidParams = {amount: string; currency: string}; - -type UnapprovedParams = {amount: string; currency: string}; -type RemoveMembersWarningPrompt = { - memberName: string; - ownerName: string; -}; - -type DeleteExpenseTranslationParams = { - count: number; -}; - -type IssueVirtualCardParams = { - assignee: string; - link: string; -}; - -type ApprovalWorkflowErrorParams = { - name1: string; - name2: string; -}; - -type AssignCardParams = { - assignee: string; - feed: string; -}; - -export type { - AddressLineParams, - AdminCanceledRequestParams, - AlreadySignedInParams, - ApprovedAmountParams, - BeginningOfChatHistoryAdminRoomPartOneParams, - BeginningOfChatHistoryAnnounceRoomPartOneParams, - BeginningOfChatHistoryAnnounceRoomPartTwo, - BeginningOfChatHistoryDomainRoomPartOneParams, - CanceledRequestParams, - CharacterLimitParams, - ConfirmHoldExpenseParams, - ConfirmThatParams, - CompanyCardFeedNameParams, - DateShouldBeAfterParams, - DateShouldBeBeforeParams, - DeleteActionParams, - DeleteConfirmationParams, - DidSplitAmountMessageParams, - DistanceRateOperationsParams, - EditActionParams, - ElectronicFundsParams, - EnglishTranslation, - EnterMagicCodeParams, - FormattedMaxLengthParams, - ForwardedAmountParams, - GoBackMessageParams, - GoToRoomParams, - HeldRequestParams, - InstantSummaryParams, - IssueVirtualCardParams, - LocalTimeParams, - LogSizeParams, - LoggedInAsParams, - ManagerApprovedAmountParams, - ManagerApprovedParams, - SignUpNewFaceCodeParams, - NoLongerHaveAccessParams, - NotAllowedExtensionParams, - NotYouParams, - OOOEventSummaryFullDayParams, - OOOEventSummaryPartialDayParams, - OurEmailProviderParams, - PaidElsewhereWithAmountParams, - PaidWithExpensifyWithAmountParams, - ParentNavigationSummaryParams, - PaySomeoneParams, - PayerOwesAmountParams, - PayerOwesParams, - PayerPaidAmountParams, - PayerPaidParams, - PayerSettledParams, - ReimbursementRateParams, - RemovedTheRequestParams, - RenamedRoomActionParams, - ReportArchiveReasonsClosedParams, - ReportArchiveReasonsMergedParams, - ReportArchiveReasonsPolicyDeletedParams, - ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams, - ReportArchiveReasonsRemovedFromPolicyParams, - RequestAmountParams, - RequestCountParams, - RequestedAmountMessageParams, - ResolutionConstraintsParams, - RoomNameReservedErrorParams, - RoomRenamedToParams, - SetTheDistanceMerchantParams, - SetTheRequestParams, - SettleExpensifyCardParams, - SettledAfterAddedBankAccountParams, - SizeExceededParams, - SplitAmountParams, - StepCounterParams, - TaskCreatedActionParams, - TermsParams, - ThreadRequestReportNameParams, - ThreadSentMoneyReportNameParams, - ToValidateLoginParams, - TransferParams, - TranslationBase, - TranslationFlatObject, - TranslationPaths, - UntilTimeParams, - UpdatedTheDistanceMerchantParams, - UpdatedTheRequestParams, - UsePlusButtonParams, - UserIsAlreadyMemberParams, - UserSplitParams, - ViolationsAutoReportedRejectedExpenseParams, - ViolationsCashExpenseWithNoReceiptParams, - ViolationsConversionSurchargeParams, - ViolationsInvoiceMarkupParams, - ViolationsMaxAgeParams, - ViolationsMissingTagParams, - ViolationsModifiedAmountParams, - ViolationsOverAutoApprovalLimitParams, - ViolationsOverCategoryLimitParams, - ViolationsOverLimitParams, - ViolationsPerDayLimitParams, - ViolationsReceiptRequiredParams, - ViolationsRterParams, - ViolationsTagOutOfPolicyParams, - ViolationsTaxOutOfPolicyParams, - WaitingOnBankAccountParams, - WalletProgramParams, - WeSentYouMagicSignInLinkParams, - WelcomeEnterMagicCodeParams, - WelcomeNoteParams, - WelcomeToRoomParams, - ZipCodeExampleFormatParams, - ChangeFieldParams, - ChangePolicyParams, - ChangeTypeParams, - ExportedToIntegrationParams, - DelegateSubmitParams, - AccountOwnerParams, - IntegrationsMessageParams, - MarkedReimbursedParams, - MarkReimbursedFromIntegrationParams, - ShareParams, - UnshareParams, - StripePaidParams, - UnapprovedParams, - RemoveMembersWarningPrompt, - DeleteExpenseTranslationParams, - ApprovalWorkflowErrorParams, - AssignCardParams, -}; +/** + * English is the default translation, other languages will be type-safe based on this + */ +type DefaultTranslation = typeof en; + +/** + * Flattened default translation object + */ +type TranslationPaths = FlattenObject; + +/** + * Flattened default translation object with its values + */ +type FlatTranslationsObject = { + [Path in TranslationPaths]: TranslationValue; +}; + +/** + * Determines the expected parameters for a specific translation function based on the provided translation path + */ +type TranslationParameters = FlatTranslationsObject[TKey] extends (...args: infer Args) => infer Return + ? Return extends PluralForm + ? Args[0] extends undefined + ? [PluralParams] + : [Args[0] & PluralParams] + : Args + : never[]; + +export type {DefaultTranslation, TranslationDeepObject, TranslationPaths, PluralForm, TranslationValue, FlatTranslationsObject, TranslationParameters}; diff --git a/src/libs/API/parameters/SetPolicyAutoReimbursementLimit.ts b/src/libs/API/parameters/SetPolicyAutoReimbursementLimit.ts index 7c6a721e03b0..b743369db926 100644 --- a/src/libs/API/parameters/SetPolicyAutoReimbursementLimit.ts +++ b/src/libs/API/parameters/SetPolicyAutoReimbursementLimit.ts @@ -1,6 +1,6 @@ type SetPolicyAutoReimbursementLimitParams = { policyID: string; - autoReimbursement: {limit: number}; + limit: number; }; export default SetPolicyAutoReimbursementLimitParams; diff --git a/src/libs/CategoryUtils.ts b/src/libs/CategoryUtils.ts index 7b2f71dbd101..f27b32360a84 100644 --- a/src/libs/CategoryUtils.ts +++ b/src/libs/CategoryUtils.ts @@ -38,10 +38,9 @@ function formatRequireReceiptsOverText(translate: LocaleContextProps['translate' const maxExpenseAmountToDisplay = policy?.maxExpenseAmount === CONST.DISABLED_MAX_EXPENSE_VALUE ? 0 : policy?.maxExpenseAmount; - return translate( - `workspace.rules.categoryRules.requireReceiptsOverList.default`, - CurrencyUtils.convertToShortDisplayString(maxExpenseAmountToDisplay, policy?.outputCurrency ?? CONST.CURRENCY.USD), - ); + return translate(`workspace.rules.categoryRules.requireReceiptsOverList.default`, { + defaultAmount: CurrencyUtils.convertToShortDisplayString(maxExpenseAmountToDisplay, policy?.outputCurrency ?? CONST.CURRENCY.USD), + }); } function getCategoryApproverRule(approvalRules: ApprovalRule[], categoryName: string) { diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index cf852e533a20..78821cde5e13 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -1,14 +1,14 @@ import mapValues from 'lodash/mapValues'; import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; -import type {TranslationFlatObject, TranslationPaths} from '@src/languages/types'; +import type {TranslationPaths} from '@src/languages/types'; import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; import type Response from '@src/types/onyx/Response'; import type {ReceiptError} from '@src/types/onyx/Transaction'; import DateUtils from './DateUtils'; import * as Localize from './Localize'; -function getAuthenticateErrorMessage(response: Response): keyof TranslationFlatObject { +function getAuthenticateErrorMessage(response: Response): TranslationPaths { switch (response.jsonCode) { case CONST.JSON_CODE.UNABLE_TO_RETRY: return 'session.offlineMessageRetry'; diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index c9eef3170245..bd8a34406846 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -6,7 +6,7 @@ import type {MessageElementBase, MessageTextElement} from '@libs/MessageElement' import Config from '@src/CONFIG'; import CONST from '@src/CONST'; import translations from '@src/languages/translations'; -import type {TranslationFlatObject, TranslationPaths} from '@src/languages/types'; +import type {PluralForm, TranslationParameters, TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Locale} from '@src/types/onyx'; import LocaleListener from './LocaleListener'; @@ -45,9 +45,6 @@ function init() { }, {}); } -type PhraseParameters = T extends (...args: infer A) => string ? A : never[]; -type Phrase = TranslationFlatObject[TKey] extends (...args: infer A) => unknown ? (...args: A) => string : string; - /** * Map to store translated values for each locale. * This is used to avoid translating the same phrase multiple times. @@ -82,17 +79,12 @@ const translationCache = new Map, Map( language: 'en' | 'es' | 'es-ES', phraseKey: TKey, - fallbackLanguage: 'en' | 'es' | null = null, - ...phraseParameters: PhraseParameters> + fallbackLanguage: 'en' | 'es' | null, + ...parameters: TranslationParameters ): string | null { // Get the cache for the above locale const cacheForLocale = translationCache.get(language); @@ -106,11 +98,44 @@ function getTranslatedPhrase( return valueFromCache; } - const translatedPhrase = translations?.[language]?.[phraseKey] as Phrase; + const translatedPhrase = translations?.[language]?.[phraseKey]; if (translatedPhrase) { if (typeof translatedPhrase === 'function') { - return translatedPhrase(...phraseParameters); + /** + * If the result of `translatedPhrase` is an object, check if it contains the 'count' parameter + * to handle pluralization logic. + * Alternatively, before evaluating the translated result, we can check if the 'count' parameter + * exists in the passed parameters. + */ + const translateFunction = translatedPhrase as unknown as (...parameters: TranslationParameters) => string | PluralForm; + const translateResult = translateFunction(...parameters); + + if (typeof translateResult === 'string') { + return translateResult; + } + + const phraseObject = parameters[0] as {count?: number}; + if (typeof phraseObject?.count !== 'number') { + throw new Error(`Invalid plural form for '${phraseKey}'`); + } + + const pluralRule = new Intl.PluralRules(language).select(phraseObject.count); + + const pluralResult = translateResult[pluralRule]; + if (pluralResult) { + if (typeof pluralResult === 'string') { + return pluralResult; + } + + return pluralResult(phraseObject.count); + } + + if (typeof translateResult.other === 'string') { + return translateResult.other; + } + + return translateResult.other(phraseObject.count); } // We set the translated value in the cache only for the phrases without parameters. @@ -123,10 +148,10 @@ function getTranslatedPhrase( } // Phrase is not found in full locale, search it in fallback language e.g. es - const fallbacktranslatedPhrase = getTranslatedPhrase(fallbackLanguage, phraseKey, null, ...phraseParameters); + const fallbackTranslatedPhrase = getTranslatedPhrase(fallbackLanguage, phraseKey, null, ...parameters); - if (fallbacktranslatedPhrase) { - return fallbacktranslatedPhrase; + if (fallbackTranslatedPhrase) { + return fallbackTranslatedPhrase; } if (fallbackLanguage !== CONST.LOCALES.DEFAULT) { @@ -134,22 +159,22 @@ function getTranslatedPhrase( } // Phrase is not translated, search it in default language (en) - return getTranslatedPhrase(CONST.LOCALES.DEFAULT, phraseKey, null, ...phraseParameters); + return getTranslatedPhrase(CONST.LOCALES.DEFAULT, phraseKey, null, ...parameters); } /** * Return translated string for given locale and phrase * * @param [desiredLanguage] eg 'en', 'es-ES' - * @param [phraseParameters] Parameters to supply if the phrase is a template literal. + * @param [parameters] Parameters to supply if the phrase is a template literal. */ -function translate(desiredLanguage: 'en' | 'es' | 'es-ES' | 'es_ES', phraseKey: TKey, ...phraseParameters: PhraseParameters>): string { +function translate(desiredLanguage: 'en' | 'es' | 'es-ES' | 'es_ES', path: TPath, ...parameters: TranslationParameters): string { // Search phrase in full locale e.g. es-ES const language = desiredLanguage === CONST.LOCALES.ES_ES_ONFIDO ? CONST.LOCALES.ES_ES : desiredLanguage; // Phrase is not found in full locale, search it in fallback language e.g. es const languageAbbreviation = desiredLanguage.substring(0, 2) as 'en' | 'es'; - const translatedPhrase = getTranslatedPhrase(language, phraseKey, languageAbbreviation, ...phraseParameters); + const translatedPhrase = getTranslatedPhrase(language, path, languageAbbreviation, ...parameters); if (translatedPhrase !== null && translatedPhrase !== undefined) { return translatedPhrase; } @@ -157,21 +182,21 @@ function translate(desiredLanguage: 'en' | 'es' | // Phrase is not found in default language, on production and staging log an alert to server // on development throw an error if (Config.IS_IN_PRODUCTION || Config.IS_IN_STAGING) { - const phraseString: string = Array.isArray(phraseKey) ? phraseKey.join('.') : phraseKey; + const phraseString = Array.isArray(path) ? path.join('.') : path; Log.alert(`${phraseString} was not found in the en locale`); if (userEmail.includes(CONST.EMAIL.EXPENSIFY_EMAIL_DOMAIN)) { return CONST.MISSING_TRANSLATION; } return phraseString; } - throw new Error(`${phraseKey} was not found in the default language`); + throw new Error(`${path} was not found in the default language`); } /** * Uses the locale in this file updated by the Onyx subscriber. */ -function translateLocal(phrase: TKey, ...variables: PhraseParameters>) { - return translate(BaseLocaleListener.getPreferredLocale(), phrase, ...variables); +function translateLocal(phrase: TPath, ...parameters: TranslationParameters) { + return translate(BaseLocaleListener.getPreferredLocale(), phrase, ...parameters); } function getPreferredListFormat(): Intl.ListFormat { @@ -226,4 +251,3 @@ function getDevicePreferredLocale(): Locale { } export {translate, translateLocal, formatList, formatMessageElementList, getDevicePreferredLocale}; -export type {PhraseParameters, Phrase}; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 92f81f9629cb..319ec60d143e 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1111,7 +1111,12 @@ const config: LinkingOptions['config'] = { }, [SCREENS.RIGHT_MODAL.EDIT_REQUEST]: { screens: { - [SCREENS.EDIT_REQUEST.REPORT_FIELD]: ROUTES.EDIT_REPORT_FIELD_REQUEST.route, + [SCREENS.EDIT_REQUEST.REPORT_FIELD]: { + path: ROUTES.EDIT_REPORT_FIELD_REQUEST.route, + parse: { + fieldID: (fieldID: string) => decodeURIComponent(fieldID), + }, + }, }, }, [SCREENS.RIGHT_MODAL.SIGN_IN]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 39cc50affaa7..39053de521db 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -68,6 +68,7 @@ type CentralPaneScreensParamList = { [SCREENS.SEARCH.CENTRAL_PANE]: { q: SearchQueryString; + name?: string; }; [SCREENS.SETTINGS.SAVE_THE_WORLD]: undefined; [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: undefined; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 2a25178f26a6..51db5a693f91 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -222,6 +222,7 @@ type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: bo type FilterOptionsConfig = Pick & { preferChatroomsOverThreads?: boolean; preferPolicyExpenseChat?: boolean; + preferRecentExpenseReports?: boolean; }; /** @@ -659,7 +660,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(TaskUtils.getTaskReportActionMessage(lastReportAction).text); } else if (ReportActionUtils.isCreatedTaskReportAction(lastReportAction)) { lastMessageTextFromReport = TaskUtils.getTaskCreatedMessage(lastReportAction); - } else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) { + } else if (ReportActionUtils.isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.SUBMITTED)) { lastMessageTextFromReport = ReportUtils.getIOUSubmittedMessage(lastReportAction); } else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED) { lastMessageTextFromReport = ReportUtils.getIOUApprovedMessage(lastReportAction); @@ -1573,7 +1574,11 @@ function createOptionFromReport(report: Report, personalDetails: OnyxEntry option?.lastIOUCreationDate ?? '' : '', + preferRecentExpenseReports ? (option) => option?.isPolicyExpenseChat : 0, ], - ['asc'], + ['asc', 'desc', 'desc'], ); } @@ -1923,6 +1937,8 @@ function getOptions( let recentReportOptions = []; let personalDetailsOptions: ReportUtils.OptionData[] = []; + const preferRecentExpenseReports = action === CONST.IOU.ACTION.CREATE; + if (includeRecentReports) { for (const reportOption of allReportOptions) { /** @@ -1983,6 +1999,22 @@ function getOptions( recentReportOptions.push(reportOption); } + // Add a field to sort the recent reports by the time of last IOU request for create actions + if (preferRecentExpenseReports) { + const reportPreviewAction = allSortedReportActions[reportOption.reportID]?.find((reportAction) => + ReportActionUtils.isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW), + ); + + if (reportPreviewAction) { + const iouReportID = ReportActionUtils.getIOUReportIDFromReportActionPreview(reportPreviewAction); + const iouReportActions = allSortedReportActions[iouReportID] ?? []; + const lastIOUAction = iouReportActions.find((iouAction) => iouAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); + if (lastIOUAction) { + reportOption.lastIOUCreationDate = lastIOUAction.lastModified; + } + } + } + // Add this login to the exclude list so it won't appear when we process the personal details if (reportOption.login) { optionsToExclude.push({login: reportOption.login}); @@ -2031,7 +2063,11 @@ function getOptions( recentReportOptions.push(...personalDetailsOptions); personalDetailsOptions = []; } - recentReportOptions = orderOptions(recentReportOptions, searchValue, {preferChatroomsOverThreads: true, preferPolicyExpenseChat: !!action}); + recentReportOptions = orderOptions(recentReportOptions, searchValue, { + preferChatroomsOverThreads: true, + preferPolicyExpenseChat: !!action, + preferRecentExpenseReports, + }); } return { @@ -2395,6 +2431,7 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt excludeLogins = [], preferChatroomsOverThreads = false, preferPolicyExpenseChat = false, + preferRecentExpenseReports = false, } = config ?? {}; if (searchInputValue.trim() === '' && maxRecentReportsToShow > 0) { return {...options, recentReports: options.recentReports.slice(0, maxRecentReportsToShow)}; @@ -2476,7 +2513,7 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt return { personalDetails, - recentReports: orderOptions(recentReports, searchValue, {preferChatroomsOverThreads, preferPolicyExpenseChat}), + recentReports: orderOptions(recentReports, searchValue, {preferChatroomsOverThreads, preferPolicyExpenseChat, preferRecentExpenseReports}), userToInvite, currentUserOption: matchResults.currentUserOption, categoryOptions: [], diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 4d13051d107c..c95f6b5a371a 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -821,7 +821,10 @@ function getCustomersOrJobsLabelNetSuite(policy: Policy | undefined, translate: importFields.push(translate('workspace.netsuite.import.customersOrJobs.jobs')); } - const importedValueLabel = translate(`workspace.netsuite.import.customersOrJobs.label`, importFields, translate(`workspace.accounting.importTypes.${importedValue}`).toLowerCase()); + const importedValueLabel = translate(`workspace.netsuite.import.customersOrJobs.label`, { + importFields, + importType: translate(`workspace.accounting.importTypes.${importedValue}`).toLowerCase(), + }); return importedValueLabel.charAt(0).toUpperCase() + importedValueLabel.slice(1); } diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f584f694edd0..8d567509c90e 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -215,8 +215,10 @@ function isActionOfType( function getOriginalMessage(reportAction: OnyxInputOrEntry>): OriginalMessage | undefined { if (!Array.isArray(reportAction?.message)) { + // eslint-disable-next-line deprecation/deprecation return reportAction?.message ?? reportAction?.originalMessage; } + // eslint-disable-next-line deprecation/deprecation return reportAction.originalMessage; } @@ -593,6 +595,7 @@ function isReportActionDeprecated(reportAction: OnyxEntry, key: st // HACK ALERT: We're temporarily filtering out any reportActions keyed by sequenceNumber // to prevent bugs during the migration from sequenceNumber -> reportActionID + // eslint-disable-next-line deprecation/deprecation if (String(reportAction.sequenceNumber) === key) { Log.info('Front-end filtered out reportAction keyed by sequenceNumber!', false, reportAction); return true; @@ -753,6 +756,18 @@ function getLastVisibleAction(reportID: string, actionsToMerge: Record 0) || (trimmedMessage === '?\u2026' && lastMessageText.length > CONST.REPORT.MIN_LENGTH_LAST_MESSAGE_WITH_ELLIPSIS)) { + return ' '; + } + + return StringUtils.lineBreaksToSpaces(trimmedMessage).substring(0, CONST.REPORT.LAST_MESSAGE_TEXT_MAX_LENGTH).trim(); +} + function getLastVisibleMessage( reportID: string, actionsToMerge: Record | null> = {}, @@ -777,7 +792,7 @@ function getLastVisibleMessage( let messageText = getReportActionMessageText(lastVisibleAction) ?? ''; if (messageText) { - messageText = StringUtils.lineBreaksToSpaces(String(messageText)).substring(0, CONST.REPORT.LAST_MESSAGE_TEXT_MAX_LENGTH).trim(); + messageText = formatLastMessageText(messageText); } return { lastMessageText: messageText, @@ -994,20 +1009,20 @@ const iouRequestTypes = new Set>([ CONST.IOU.REPORT_ACTION_TYPE.TRACK, ]); -/** - * Gets the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions. - * Returns a reportID if there is exactly one transaction thread for the report, and null otherwise. - */ -function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[], isOffline: boolean | undefined = undefined): string | undefined { - // If the report is not an IOU, Expense report, or Invoice, it shouldn't be treated as one-transaction report. +function getMoneyRequestActions( + reportID: string, + reportActions: OnyxEntry | ReportAction[], + isOffline: boolean | undefined = undefined, +): Array> { + // If the report is not an IOU, Expense report, or Invoice, it shouldn't have money request actions. const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; if (report?.type !== CONST.REPORT.TYPE.IOU && report?.type !== CONST.REPORT.TYPE.EXPENSE && report?.type !== CONST.REPORT.TYPE.INVOICE) { - return; + return []; } const reportActionsArray = Array.isArray(reportActions) ? reportActions : Object.values(reportActions ?? {}); if (!reportActionsArray.length) { - return; + return []; } const iouRequestActions = []; @@ -1035,6 +1050,15 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn iouRequestActions.push(action); } } + return iouRequestActions; +} + +/** + * Gets the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions. + * Returns a reportID if there is exactly one transaction thread for the report, and null otherwise. + */ +function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[], isOffline: boolean | undefined = undefined): string | undefined { + const iouRequestActions = getMoneyRequestActions(reportID, reportActions, isOffline); // If we don't have any IOU request actions, or we have more than one IOU request actions, this isn't a oneTransaction report if (!iouRequestActions.length || iouRequestActions.length > 1) { @@ -1054,6 +1078,27 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn return singleAction.childReportID; } +/** + * Returns true if all transactions on the report have the same ownerID + */ +function hasSameActorForAllTransactions(reportID: string, reportActions: OnyxEntry | ReportAction[], isOffline: boolean | undefined = undefined): boolean { + const iouRequestActions = getMoneyRequestActions(reportID, reportActions, isOffline); + if (!iouRequestActions.length) { + return true; + } + + let actorID: number | undefined; + + for (const action of iouRequestActions) { + if (actorID !== undefined && actorID !== action?.actorAccountID) { + return false; + } + actorID = action?.actorAccountID; + } + + return true; +} + /** * When we delete certain reports, we want to check whether there are any visible actions left to display. * If there are no visible actions left (including system messages), we can hide the report from view entirely @@ -1266,7 +1311,7 @@ function getMessageOfOldDotReportAction(oldDotAction: PartialReportAction | OldD case CONST.REPORT.ACTIONS.TYPE.INTEGRATIONS_MESSAGE: { const {result, label} = originalMessage; const errorMessage = result?.messages?.join(', ') ?? ''; - return Localize.translateLocal('report.actions.type.integrationsMessage', errorMessage, label); + return Localize.translateLocal('report.actions.type.integrationsMessage', {errorMessage, label}); } case CONST.REPORT.ACTIONS.TYPE.MANAGER_ATTACH_RECEIPT: return Localize.translateLocal('report.actions.type.managerAttachReceipt'); @@ -1643,7 +1688,7 @@ function getPolicyChangeLogAddEmployeeMessage(reportAction: OnyxInputOrEntry): reportAction is ReportAction { @@ -1658,7 +1703,7 @@ function getPolicyChangeLogChangeRoleMessage(reportAction: OnyxInputOrEntry>) { @@ -1698,7 +1743,7 @@ function isCardIssuedAction(reportAction: OnyxEntry) { } function getCardIssuedMessage(reportAction: OnyxEntry, shouldRenderHTML = false) { - const assigneeAccountID = (reportAction?.originalMessage as IssueNewCardOriginalMessage)?.assigneeAccountID; + const assigneeAccountID = (getOriginalMessage(reportAction) as IssueNewCardOriginalMessage)?.assigneeAccountID; const assigneeDetails = PersonalDetailsUtils.getPersonalDetailsByIDs([assigneeAccountID], currentUserAccountID ?? -1)[0]; const assignee = shouldRenderHTML ? `` : assigneeDetails?.firstName ?? assigneeDetails.login ?? ''; @@ -1719,11 +1764,11 @@ function getCardIssuedMessage(reportAction: OnyxEntry, shouldRende const shouldShowAddMissingDetailsButton = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS && missingDetails && isAssigneeCurrentUser; switch (reportAction?.actionName) { case CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED: - return Localize.translateLocal('workspace.expensifyCard.issuedCard', assignee); + return Localize.translateLocal('workspace.expensifyCard.issuedCard', {assignee}); case CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED_VIRTUAL: return Localize.translateLocal('workspace.expensifyCard.issuedCardVirtual', {assignee, link}); case CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS: - return Localize.translateLocal(`workspace.expensifyCard.${shouldShowAddMissingDetailsButton ? 'issuedCardNoShippingDetails' : 'addedShippingDetails'}`, assignee); + return Localize.translateLocal(`workspace.expensifyCard.${shouldShowAddMissingDetailsButton ? 'issuedCardNoShippingDetails' : 'addedShippingDetails'}`, {assignee}); default: return ''; } @@ -1732,6 +1777,7 @@ function getCardIssuedMessage(reportAction: OnyxEntry, shouldRende export { doesReportHaveVisibleActions, extractLinksFromMessageHtml, + formatLastMessageText, getActionableMentionWhisperMessage, getAllReportActions, getCombinedReportActions, @@ -1754,6 +1800,7 @@ export { getNumberOfMoneyRequests, getOneTransactionThreadReportID, getOriginalMessage, + // eslint-disable-next-line deprecation/deprecation getParentReportAction, getRemovedFromApprovalChainMessage, getReportAction, @@ -1834,6 +1881,7 @@ export { getRenamedAction, isCardIssuedAction, getCardIssuedMessage, + hasSameActorForAllTransactions, }; export type {LastVisibleMessage}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 896aa2b3e634..684274bc0079 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -19,7 +19,8 @@ import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvata import type {MoneyRequestAmountInputProps} from '@components/MoneyRequestAmountInput'; import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; -import type {ParentNavigationSummaryParams, TranslationPaths} from '@src/languages/types'; +import type {ParentNavigationSummaryParams} from '@src/languages/params'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; @@ -83,7 +84,6 @@ import * as PolicyUtils from './PolicyUtils'; import type {LastVisibleMessage} from './ReportActionsUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; import * as ReportConnection from './ReportConnection'; -import StringUtils from './StringUtils'; import * as TransactionUtils from './TransactionUtils'; import * as Url from './Url'; import type {AvatarSource} from './UserUtils'; @@ -127,6 +127,7 @@ type OptimisticAddCommentReportAction = Pick< | 'childCommenterCount' | 'childLastVisibleActionCreated' | 'childOldestFourAccountIDs' + | 'delegateAccountID' > & {isOptimisticAction: boolean}; type OptimisticReportAction = { @@ -180,6 +181,7 @@ type OptimisticIOUReportAction = Pick< | 'childReportID' | 'childVisibleActionCount' | 'childCommenterCount' + | 'delegateAccountID' >; type PartialReportAction = OnyxInputOrEntry | Partial | OptimisticIOUReportAction | OptimisticApprovedReportAction | OptimisticSubmittedReportAction | undefined; @@ -196,12 +198,36 @@ type ReportOfflinePendingActionAndErrors = { type OptimisticApprovedReportAction = Pick< ReportAction, - 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachmentOnly' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' + | 'actionName' + | 'actorAccountID' + | 'automatic' + | 'avatar' + | 'isAttachmentOnly' + | 'originalMessage' + | 'message' + | 'person' + | 'reportActionID' + | 'shouldShow' + | 'created' + | 'pendingAction' + | 'delegateAccountID' >; type OptimisticUnapprovedReportAction = Pick< ReportAction, - 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachmentOnly' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' + | 'actionName' + | 'actorAccountID' + | 'automatic' + | 'avatar' + | 'isAttachmentOnly' + | 'originalMessage' + | 'message' + | 'person' + | 'reportActionID' + | 'shouldShow' + | 'created' + | 'pendingAction' + | 'delegateAccountID' >; type OptimisticSubmittedReportAction = Pick< @@ -219,6 +245,7 @@ type OptimisticSubmittedReportAction = Pick< | 'shouldShow' | 'created' | 'pendingAction' + | 'delegateAccountID' >; type OptimisticHoldReportAction = Pick< @@ -233,7 +260,7 @@ type OptimisticCancelPaymentReportAction = Pick< type OptimisticEditedTaskReportAction = Pick< ReportAction, - 'reportActionID' | 'actionName' | 'pendingAction' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'shouldShow' | 'message' | 'person' + 'reportActionID' | 'actionName' | 'pendingAction' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'shouldShow' | 'message' | 'person' | 'delegateAccountID' >; type OptimisticClosedReportAction = Pick< @@ -248,7 +275,7 @@ type OptimisticDismissedViolationReportAction = Pick< type OptimisticCreatedReportAction = Pick< ReportAction, - 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'pendingAction' | 'actionName' + 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'pendingAction' | 'actionName' | 'delegateAccountID' >; type OptimisticRenamedReportAction = Pick< @@ -321,6 +348,7 @@ type OptimisticTaskReportAction = Pick< | 'previousMessage' | 'errors' | 'linkMetadata' + | 'delegateAccountID' >; type OptimisticWorkspaceChats = { @@ -340,7 +368,19 @@ type OptimisticWorkspaceChats = { type OptimisticModifiedExpenseReportAction = Pick< ReportAction, - 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'isAttachmentOnly' | 'message' | 'originalMessage' | 'person' | 'pendingAction' | 'reportActionID' | 'shouldShow' + | 'actionName' + | 'actorAccountID' + | 'automatic' + | 'avatar' + | 'created' + | 'isAttachmentOnly' + | 'message' + | 'originalMessage' + | 'person' + | 'pendingAction' + | 'reportActionID' + | 'shouldShow' + | 'delegateAccountID' > & {reportID?: string}; type OptimisticTaskReport = Pick< @@ -459,6 +499,7 @@ type OptionData = { tabIndex?: 0 | -1; isConciergeChat?: boolean; isBold?: boolean; + lastIOUCreationDate?: string; } & Report; type OnyxDataTaskAssigneeChat = { @@ -636,6 +677,14 @@ Onyx.connect({ callback: (value) => (onboarding = value), }); +let delegateEmail = ''; +Onyx.connect({ + key: ONYXKEYS.ACCOUNT, + callback: (value) => { + delegateEmail = value?.delegatedAccess?.delegate ?? ''; + }, +}); + function getCurrentUserAvatar(): AvatarSource | undefined { return currentUserPersonalDetails?.avatar; } @@ -1580,12 +1629,20 @@ function hasOnlyNonReimbursableTransactions(iouReportID: string | undefined): bo return transactions.every((transaction) => !TransactionUtils.getReimbursable(transaction)); } +/** + * Checks if a report has only transactions with same ownerID + */ +function isSingleActorMoneyReport(reportID: string): boolean { + const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? ([] as ReportAction[]); + return !!ReportActionsUtils.hasSameActorForAllTransactions(reportID, reportActions); +} + /** * Checks if a report has only one transaction associated with it */ function isOneTransactionReport(reportID: string): boolean { const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? ([] as ReportAction[]); - return ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions) !== null; + return !!ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions); } /* @@ -1830,7 +1887,8 @@ function formatReportLastMessageText(lastMessageText: string, isModifiedExpenseM if (isModifiedExpenseMessage) { return String(lastMessageText).trim().replace(CONST.REGEX.LINE_BREAK, '').trim(); } - return StringUtils.lineBreaksToSpaces(String(lastMessageText).trim()).substring(0, CONST.REPORT.LAST_MESSAGE_TEXT_MAX_LENGTH).trim(); + + return ReportActionsUtils.formatLastMessageText(lastMessageText); } /** @@ -2217,7 +2275,7 @@ function getIcons( if (isChatThread(report)) { const parentReportAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID]; - const actorAccountID = getReportActionActorAccountID(parentReportAction, report); + const actorAccountID = getReportActionActorAccountID(parentReportAction); const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[actorAccountID ?? -1], '', false); const actorIcon = { id: actorAccountID, @@ -2312,7 +2370,7 @@ function getIcons( const isManager = currentUserAccountID === report?.managerID; // For one transaction IOUs, display a simplified report icon - if (isOneTransactionReport(report?.reportID ?? '-1')) { + if (isOneTransactionReport(report?.reportID ?? '-1') || isSingleActorMoneyReport(report?.reportID ?? '-1')) { return [ownerIcon]; } @@ -3127,7 +3185,7 @@ function canHoldUnholdReportAction(reportAction: OnyxInputOrEntry) return {canHoldRequest, canUnholdRequest}; } -const changeMoneyRequestHoldStatus = (reportAction: OnyxEntry, backTo?: string): void => { +const changeMoneyRequestHoldStatus = (reportAction: OnyxEntry, backTo?: string, searchHash?: number): void => { if (!ReportActionsUtils.isMoneyRequestAction(reportAction)) { return; } @@ -3144,11 +3202,13 @@ const changeMoneyRequestHoldStatus = (reportAction: OnyxEntry, bac const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${moneyRequestReport.policyID}`] ?? null; if (isOnHold) { - IOU.unholdRequest(transactionID, reportAction.childReportID ?? ''); + IOU.unholdRequest(transactionID, reportAction.childReportID ?? '', searchHash); } else { const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams()); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? CONST.POLICY.TYPE.PERSONAL, transactionID, reportAction.childReportID ?? '', backTo || activeRoute)); + Navigation.navigate( + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? CONST.POLICY.TYPE.PERSONAL, transactionID, reportAction.childReportID ?? '', backTo || activeRoute, searchHash), + ); } }; @@ -3713,7 +3773,7 @@ function getReportName( } const parentReportActionMessage = ReportActionsUtils.getReportActionMessage(parentReportAction); - if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) { + if (ReportActionsUtils.isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.SUBMITTED)) { return getIOUSubmittedMessage(parentReportAction); } if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.FORWARDED) { @@ -4137,6 +4197,7 @@ function buildOptimisticAddCommentReportAction( const isAttachmentOnly = file && !text; const isAttachmentWithText = !!text && file !== undefined; const accountID = actorAccountID ?? currentUserAccountID ?? -1; + const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail); // Remove HTML from text when applying optimistic offline comment return { @@ -4173,6 +4234,7 @@ function buildOptimisticAddCommentReportAction( pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, shouldShow: true, isOptimisticAction: true, + delegateAccountID: delegateAccountDetails?.accountID, }, }; } @@ -4448,7 +4510,11 @@ function getFormattedAmount(reportAction: ReportAction) { return formattedAmount; } -function getIOUSubmittedMessage(reportAction: ReportAction) { +function getReportAutomaticallySubmittedMessage(reportAction: ReportAction) { + return Localize.translateLocal('iou.automaticallySubmittedAmount', {formattedAmount: getFormattedAmount(reportAction)}); +} + +function getIOUSubmittedMessage(reportAction: ReportAction) { return Localize.translateLocal('iou.submittedAmount', {formattedAmount: getFormattedAmount(reportAction)}); } @@ -4597,6 +4663,8 @@ function buildOptimisticIOUReportAction( type, }; + const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail); + if (type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { // In pay someone flow, we store amount, comment, currency in IOUDetails when type = pay if (isSendMoneyFlow) { @@ -4648,6 +4716,7 @@ function buildOptimisticIOUReportAction( shouldShow: true, created, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + delegateAccountID: delegateAccountDetails?.accountID, }; } @@ -4660,6 +4729,7 @@ function buildOptimisticApprovedReportAction(amount: number, currency: string, e currency, expenseReportID, }; + const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail); return { actionName: CONST.REPORT.ACTIONS.TYPE.APPROVED, @@ -4680,6 +4750,7 @@ function buildOptimisticApprovedReportAction(amount: number, currency: string, e shouldShow: true, created: DateUtils.getDBTime(), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + delegateAccountID: delegateAccountDetails?.accountID, }; } @@ -4687,6 +4758,7 @@ function buildOptimisticApprovedReportAction(amount: number, currency: string, e * Builds an optimistic APPROVED report action with a randomly generated reportActionID. */ function buildOptimisticUnapprovedReportAction(amount: number, currency: string, expenseReportID: string): OptimisticUnapprovedReportAction { + const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail); return { actionName: CONST.REPORT.ACTIONS.TYPE.UNAPPROVED, actorAccountID: currentUserAccountID, @@ -4710,6 +4782,7 @@ function buildOptimisticUnapprovedReportAction(amount: number, currency: string, shouldShow: true, created: DateUtils.getDBTime(), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + delegateAccountID: delegateAccountDetails?.accountID, }; } @@ -4766,6 +4839,8 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string, expenseReportID, }; + const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail); + return { actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, actorAccountID: currentUserAccountID, @@ -4786,6 +4861,7 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string, shouldShow: true, created: DateUtils.getDBTime(), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + delegateAccountID: delegateAccountDetails?.accountID, }; } @@ -4826,9 +4902,9 @@ function buildOptimisticReportPreview( }, ], created, - accountID: iouReport?.managerID ?? -1, + accountID: iouReport?.ownerAccountID ?? -1, // The preview is initially whispered if created with a receipt, so the actor is the current user as well - actorAccountID: hasReceipt ? currentUserAccountID : iouReport?.managerID ?? -1, + actorAccountID: hasReceipt ? currentUserAccountID : iouReport?.ownerAccountID ?? -1, childReportID: childReportID ?? iouReport?.reportID, childMoneyRequestCount: 1, childLastMoneyRequestComment: comment, @@ -4886,6 +4962,8 @@ function buildOptimisticModifiedExpenseReportAction( updatedTransaction?: OnyxInputOrEntry, ): OptimisticModifiedExpenseReportAction { const originalMessage = getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, isFromExpenseReport, policy, updatedTransaction); + const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail); + return { actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, actorAccountID: currentUserAccountID, @@ -4913,6 +4991,7 @@ function buildOptimisticModifiedExpenseReportAction( reportActionID: NumberUtils.rand64(), reportID: transactionThread?.reportID, shouldShow: true, + delegateAccountID: delegateAccountDetails?.accountID, }; } @@ -4922,6 +5001,8 @@ function buildOptimisticModifiedExpenseReportAction( * @param movedToReportID - The reportID of the report the transaction is moved to */ function buildOptimisticMovedTrackedExpenseModifiedReportAction(transactionThreadID: string, movedToReportID: string): OptimisticModifiedExpenseReportAction { + const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail); + return { actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, actorAccountID: currentUserAccountID, @@ -4951,6 +5032,7 @@ function buildOptimisticMovedTrackedExpenseModifiedReportAction(transactionThrea reportActionID: NumberUtils.rand64(), reportID: transactionThreadID, shouldShow: true, + delegateAccountID: delegateAccountDetails?.accountID, }; } @@ -5026,6 +5108,8 @@ function buildOptimisticTaskReportAction( html: message, whisperedTo: [], }; + const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail); + return { actionName, actorAccountID, @@ -5052,6 +5136,7 @@ function buildOptimisticTaskReportAction( created: DateUtils.getDBTimeWithSkew(Date.now() + createdOffset), isFirstItem: false, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + delegateAccountID: delegateAccountDetails?.accountID, }; } @@ -5381,6 +5466,7 @@ function buildOptimisticEditedTaskFieldReportAction({title, description}: Task): } else if (field) { changelog = `removed the ${field}`; } + const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail); return { reportActionID: NumberUtils.rand64(), @@ -5405,10 +5491,13 @@ function buildOptimisticEditedTaskFieldReportAction({title, description}: Task): avatar: getCurrentUserAvatar(), created: DateUtils.getDBTime(), shouldShow: false, + delegateAccountID: delegateAccountDetails?.accountID, }; } function buildOptimisticChangedTaskAssigneeReportAction(assigneeAccountID: number): OptimisticEditedTaskReportAction { + const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail); + return { reportActionID: NumberUtils.rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.TASK_EDITED, @@ -5432,6 +5521,7 @@ function buildOptimisticChangedTaskAssigneeReportAction(assigneeAccountID: numbe avatar: getCurrentUserAvatar(), created: DateUtils.getDBTime(), shouldShow: false, + delegateAccountID: delegateAccountDetails?.accountID, }; } @@ -6649,7 +6739,7 @@ function shouldReportShowSubscript(report: OnyxEntry): boolean { return true; } - if (isExpenseReport(report) && isOneTransactionReport(report?.reportID ?? '-1')) { + if (isExpenseReport(report)) { return true; } @@ -6661,7 +6751,7 @@ function shouldReportShowSubscript(report: OnyxEntry): boolean { return true; } - if (isInvoiceRoom(report)) { + if (isInvoiceRoom(report) || isInvoiceReport(report)) { return true; } @@ -7543,10 +7633,10 @@ function canLeaveChat(report: OnyxEntry, policy: OnyxEntry): boo return (isChatThread(report) && !!getReportNotificationPreference(report)) || isUserCreatedPolicyRoom(report) || isNonAdminOrOwnerOfPolicyExpenseChat(report, policy); } -function getReportActionActorAccountID(reportAction: OnyxInputOrEntry, iouReport: OnyxInputOrEntry | undefined): number | undefined { +function getReportActionActorAccountID(reportAction: OnyxInputOrEntry): number | undefined { switch (reportAction?.actionName) { case CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW: - return !isEmptyObject(iouReport) ? iouReport.managerID : reportAction?.childManagerAccountID; + return reportAction?.childOwnerAccountID ?? reportAction?.actorAccountID; case CONST.REPORT.ACTIONS.TYPE.SUBMITTED: return reportAction?.adminAccountID ?? reportAction?.actorAccountID; @@ -7701,7 +7791,7 @@ function getFieldViolationTranslation(reportField: PolicyReportField, violation? switch (violation) { case 'fieldRequired': - return Localize.translateLocal('reportViolations.fieldRequired', reportField.name); + return Localize.translateLocal('reportViolations.fieldRequired', {fieldName: reportField.name}); default: return ''; } @@ -7917,6 +8007,7 @@ export { getIOUForwardedMessage, getRejectedReportMessage, getWorkspaceNameUpdatedMessage, + getReportAutomaticallySubmittedMessage, getIOUSubmittedMessage, getIcons, getIconsForParticipants, @@ -8127,6 +8218,7 @@ export { isIndividualInvoiceRoom, isAuditor, hasMissingInvoiceBankAccount, + isSingleActorMoneyReport, }; export type { diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index e178c7dcb77b..0218c17cdac6 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -784,7 +784,7 @@ function getOverflowMenu(itemName: string, hash: number, inputQuery: string, sho if (isMobileMenu && closeMenu) { closeMenu(); } - Navigation.navigate(ROUTES.SEARCH_SAVED_SEARCH_RENAME.getRoute({name: itemName, jsonQuery: inputQuery})); + Navigation.navigate(ROUTES.SEARCH_SAVED_SEARCH_RENAME.getRoute({name: encodeURIComponent(itemName), jsonQuery: inputQuery})); }, icon: Expensicons.Pencil, shouldShowRightIcon: false, diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index adbc05460220..c7ee0a0b0867 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -1,11 +1,10 @@ import reject from 'lodash/reject'; import Onyx from 'react-native-onyx'; import type {OnyxUpdate} from 'react-native-onyx'; -import type {Phrase, PhraseParameters} from '@libs/Localize'; +import type {LocaleContextProps} from '@components/LocaleContextProvider'; import {getCustomUnitRate, getSortedTagKeys} from '@libs/PolicyUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyCategories, PolicyTagLists, Transaction, TransactionViolation, ViolationName} from '@src/types/onyx'; @@ -237,10 +236,7 @@ const ViolationsUtils = { * possible values could be either translation keys that resolve to strings or translation keys that resolve to * functions. */ - getViolationTranslation( - violation: TransactionViolation, - translate: (phraseKey: TKey, ...phraseParameters: PhraseParameters>) => string, - ): string { + getViolationTranslation(violation: TransactionViolation, translate: LocaleContextProps['translate']): string { const { brokenBankConnection = false, isAdmin = false, @@ -250,7 +246,7 @@ const ViolationsUtils = { category, rejectedBy = '', rejectReason = '', - formattedLimit, + formattedLimit = '', surcharge = 0, invoiceMarkup = 0, maxAge = 0, diff --git a/src/libs/WorkspacesSettingsUtils.ts b/src/libs/WorkspacesSettingsUtils.ts index ed46b0b5f5ec..d8cd2ff00828 100644 --- a/src/libs/WorkspacesSettingsUtils.ts +++ b/src/libs/WorkspacesSettingsUtils.ts @@ -1,6 +1,7 @@ import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import type {LocaleContextProps} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -8,7 +9,6 @@ import type {Policy, ReimbursementAccount, Report, ReportAction, ReportActions, import type {PolicyConnectionSyncProgress, Unit} from '@src/types/onyx/Policy'; import {isConnectionInProgress} from './actions/connections'; import * as CurrencyUtils from './CurrencyUtils'; -import type {Phrase, PhraseParameters} from './Localize'; import * as OptionsListUtils from './OptionsListUtils'; import {hasCustomUnitsError, hasEmployeeListError, hasPolicyError, hasSyncError, hasTaxRateError} from './PolicyUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; @@ -234,7 +234,7 @@ function getUnitTranslationKey(unit: Unit): TranslationPaths { */ function getOwnershipChecksDisplayText( error: ValueOf, - translate: (phraseKey: TKey, ...phraseParameters: PhraseParameters>) => string, + translate: LocaleContextProps['translate'], policy: OnyxEntry, accountLogin: string | undefined, ) { @@ -271,14 +271,14 @@ function getOwnershipChecksDisplayText( case CONST.POLICY.OWNERSHIP_ERRORS.DUPLICATE_SUBSCRIPTION: title = translate('workspace.changeOwner.duplicateSubscriptionTitle'); text = translate('workspace.changeOwner.duplicateSubscriptionText', { - email: changeOwner?.duplicateSubscription, - workspaceName: policy?.name, + email: changeOwner?.duplicateSubscription ?? '', + workspaceName: policy?.name ?? '', }); buttonText = translate('workspace.changeOwner.duplicateSubscriptionButtonText'); break; case CONST.POLICY.OWNERSHIP_ERRORS.HAS_FAILED_SETTLEMENTS: title = translate('workspace.changeOwner.hasFailedSettlementsTitle'); - text = translate('workspace.changeOwner.hasFailedSettlementsText', {email: accountLogin}); + text = translate('workspace.changeOwner.hasFailedSettlementsText', {email: accountLogin ?? ''}); buttonText = translate('workspace.changeOwner.hasFailedSettlementsButtonText'); break; case CONST.POLICY.OWNERSHIP_ERRORS.FAILED_TO_CLEAR_BALANCE: diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index c422313a1946..bc553ea86d70 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -65,6 +65,7 @@ import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type ReportAction from '@src/types/onyx/ReportAction'; import type {OnyxData} from '@src/types/onyx/Request'; +import type {SearchTransaction} from '@src/types/onyx/SearchResults'; import type {Comment, Receipt, ReceiptSource, Routes, SplitShares, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as CachedPDFPaths from './CachedPDFPaths'; @@ -7856,7 +7857,7 @@ function adjustRemainingSplitShares(transaction: NonNullable>>, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`, + value: { + data: { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: { + canHold: true, + canUnhold: false, + }, + }, + } as Record>>, + }); + } + API.write( 'HoldRequest', { @@ -7928,7 +7957,7 @@ function putOnHold(transactionID: string, comment: string, reportID: string) { /** * Remove expense from HOLD */ -function unholdRequest(transactionID: string, reportID: string) { +function unholdRequest(transactionID: string, reportID: string, searchHash?: number) { const createdReportAction = ReportUtils.buildOptimisticUnHoldReportAction(); const transactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; @@ -7986,6 +8015,34 @@ function unholdRequest(transactionID: string, reportID: string) { }, ]; + // If we are unholding from the search page, we optimistically update the snapshot data that search uses so that it is kept in sync + if (searchHash) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`, + value: { + data: { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: { + canHold: true, + canUnhold: false, + }, + }, + } as Record>>, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`, + value: { + data: { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: { + canHold: false, + canUnhold: true, + }, + }, + } as Record>>, + }); + } + API.write( 'UnHoldRequest', { diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index c342fe6eedb6..a9f6e376b80a 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -153,7 +153,7 @@ function updateImportSpreadsheetData(categoriesLength: number) { shouldFinalModalBeOpened: true, importFinalModal: { title: translateLocal('spreadsheet.importSuccessfullTitle'), - prompt: translateLocal('spreadsheet.importCategoriesSuccessfullDescription', categoriesLength), + prompt: translateLocal('spreadsheet.importCategoriesSuccessfullDescription', {categories: categoriesLength}), }, }, }, diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index 44ce9ea6f91c..8c2a66a8ccf6 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -183,7 +183,10 @@ function updateImportSpreadsheetData(membersLength: number): OnyxData { key: ONYXKEYS.IMPORTED_SPREADSHEET, value: { shouldFinalModalBeOpened: true, - importFinalModal: {title: translateLocal('spreadsheet.importSuccessfullTitle'), prompt: translateLocal('spreadsheet.importMembersSuccessfullDescription', membersLength)}, + importFinalModal: { + title: translateLocal('spreadsheet.importSuccessfullTitle'), + prompt: translateLocal('spreadsheet.importMembersSuccessfullDescription', {members: membersLength}), + }, }, }, ], diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index db4fa4d417f6..8f8bba8e916f 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -4160,9 +4160,10 @@ function setPolicyAutomaticApprovalLimit(policyID: string, limit: string) { function setPolicyAutomaticApprovalRate(policyID: string, auditRate: string) { const policy = getPolicy(policyID); const fallbackAuditRate = auditRate === '' ? '0' : auditRate; - const parsedAuditRate = parseInt(fallbackAuditRate, 10); + const parsedAuditRate = parseInt(fallbackAuditRate, 10) / 100; - if (parsedAuditRate === policy?.autoApproval?.auditRate ?? CONST.POLICY.RANDOM_AUDIT_DEFAULT_PERCENTAGE) { + // The auditRate arrives as an int to this method so we will convert it to a float before sending it to the API. + if (parsedAuditRate === (policy?.autoApproval?.auditRate ?? CONST.POLICY.RANDOM_AUDIT_DEFAULT_PERCENTAGE)) { return; } @@ -4238,17 +4239,8 @@ function enableAutoApprovalOptions(policyID: string, enabled: boolean) { return; } - const autoApprovalCleanupValues = !enabled - ? { - pendingFields: { - limit: null, - auditRate: null, - }, - } - : {}; - const autoApprovalValues = !enabled ? {auditRate: CONST.POLICY.RANDOM_AUDIT_DEFAULT_PERCENTAGE, limit: CONST.POLICY.AUTO_APPROVE_REPORTS_UNDER_DEFAULT_CENTS} : {}; - const autoApprovalFailureValues = !enabled ? {autoApproval: {limit: policy?.autoApproval?.limit, auditRate: policy?.autoApproval?.auditRate, ...autoApprovalCleanupValues}} : {}; - + const autoApprovalValues = {auditRate: CONST.POLICY.RANDOM_AUDIT_DEFAULT_PERCENTAGE, limit: CONST.POLICY.AUTO_APPROVE_REPORTS_UNDER_DEFAULT_CENTS}; + const autoApprovalFailureValues = {autoApproval: {limit: policy?.autoApproval?.limit, auditRate: policy?.autoApproval?.auditRate, pendingFields: null}}; const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -4274,7 +4266,7 @@ function enableAutoApprovalOptions(policyID: string, enabled: boolean) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - autoApproval: {...autoApprovalCleanupValues}, + autoApproval: {pendingFields: null}, pendingFields: { shouldShowAutoApprovalOptions: null, }, @@ -4367,7 +4359,7 @@ function setPolicyAutoReimbursementLimit(policyID: string, limit: string) { ]; const parameters: SetPolicyAutoReimbursementLimitParams = { - autoReimbursement: {limit: parsedLimit}, + limit: parsedLimit, policyID, }; @@ -4380,6 +4372,7 @@ function setPolicyAutoReimbursementLimit(policyID: string, limit: string) { /** * Call the API to enable auto-payment for the reports in the given policy + * * @param policyID - id of the policy to apply the limit to * @param enabled - whether auto-payment for the reports is enabled in the given policy */ @@ -4390,16 +4383,8 @@ function enablePolicyAutoReimbursementLimit(policyID: string, enabled: boolean) return; } - const autoReimbursementCleanupValues = !enabled - ? { - pendingFields: { - limit: null, - }, - } - : {}; - const autoReimbursementFailureValues = !enabled ? {autoReimbursement: {limit: policy?.autoReimbursement?.limit, ...autoReimbursementCleanupValues}} : {}; - const autoReimbursementValues = !enabled ? {limit: CONST.POLICY.AUTO_REIMBURSEMENT_DEFAULT_LIMIT_CENTS} : {}; - + const autoReimbursementFailureValues = {autoReimbursement: {limit: policy?.autoReimbursement?.limit, pendingFields: null}}; + const autoReimbursementValues = {limit: CONST.POLICY.AUTO_REIMBURSEMENT_DEFAULT_LIMIT_CENTS}; const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -4424,7 +4409,7 @@ function enablePolicyAutoReimbursementLimit(policyID: string, enabled: boolean) onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - autoReimbursement: {...autoReimbursementCleanupValues}, + autoReimbursement: {pendingFields: null}, pendingFields: { shouldShowAutoReimbursementLimitOption: null, }, diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index f2cd818fd6c1..9628b6ceda77 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -136,7 +136,10 @@ function updateImportSpreadsheetData(tagsLength: number): OnyxData { key: ONYXKEYS.IMPORTED_SPREADSHEET, value: { shouldFinalModalBeOpened: true, - importFinalModal: {title: translateLocal('spreadsheet.importSuccessfullTitle'), prompt: translateLocal('spreadsheet.importTagsSuccessfullDescription', tagsLength)}, + importFinalModal: { + title: translateLocal('spreadsheet.importSuccessfullTitle'), + prompt: translateLocal('spreadsheet.importTagsSuccessfullDescription', {tags: tagsLength}), + }, }, }, ], diff --git a/src/libs/actions/PriorityMode.ts b/src/libs/actions/PriorityMode.ts index beec327a2e40..2aca5d9f9de8 100644 --- a/src/libs/actions/PriorityMode.ts +++ b/src/libs/actions/PriorityMode.ts @@ -57,11 +57,11 @@ Onyx.connect({ }, }); -let hasTriedFocusMode: boolean | null | undefined; +let hasTriedFocusMode: boolean | undefined; Onyx.connect({ key: ONYXKEYS.NVP_TRY_FOCUS_MODE, callback: (val) => { - hasTriedFocusMode = val ?? null; + hasTriedFocusMode = val; // eslint-disable-next-line @typescript-eslint/no-use-before-define checkRequiredData(); diff --git a/src/libs/actions/connections/QuickbooksOnline.ts b/src/libs/actions/connections/QuickbooksOnline.ts index c62c97aa88ca..bb85c8f5223f 100644 --- a/src/libs/actions/connections/QuickbooksOnline.ts +++ b/src/libs/actions/connections/QuickbooksOnline.ts @@ -384,7 +384,7 @@ function updateQuickbooksOnlinePreferredExporter ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, reportActions ?? [], isOffline), @@ -491,10 +493,11 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { /> ) : null; - const connectedIntegrationName = connectedIntegration ? translate('workspace.accounting.connectionName', connectedIntegration) : ''; + const connectedIntegrationName = connectedIntegration ? translate('workspace.accounting.connectionName', {connectionName: connectedIntegration}) : ''; const unapproveWarningText = ( - {translate('iou.headsUp')} {translate('iou.unapproveWithIntegrationWarning', connectedIntegrationName)} + {translate('iou.headsUp')}{' '} + {translate('iou.unapproveWithIntegrationWarning', {accountingIntegration: connectedIntegrationName})} ); @@ -571,6 +574,7 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { reportID: transactionThreadReportID ? report.reportID : moneyRequestAction?.childReportID ?? '-1', isDelegateAccessRestricted, setIsNoDelegateAccessMenuVisible, + currentSearchHash, }), ); } @@ -582,7 +586,18 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { result.push(PromotedActions.share(report, backTo)); return result; - }, [report, moneyRequestAction, canJoin, isExpenseReport, shouldShowHoldAction, canHoldUnholdReportAction.canHoldRequest, transactionThreadReportID, isDelegateAccessRestricted, backTo]); + }, [ + report, + moneyRequestAction, + currentSearchHash, + canJoin, + isExpenseReport, + shouldShowHoldAction, + canHoldUnholdReportAction.canHoldRequest, + transactionThreadReportID, + isDelegateAccessRestricted, + backTo, + ]); const nameSectionExpenseIOU = ( diff --git a/src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx b/src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx index 89b5dcdd8a2b..342cd4ce5e6e 100644 --- a/src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx +++ b/src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx @@ -50,10 +50,10 @@ function WorkspaceAdminRestrictedAction({policyID}: WorkspaceAdminRestrictedActi height={variables.restrictedActionIllustrationHeight} /> - {translate('workspace.restrictedAction.actionsAreCurrentlyRestricted', {workspaceName: policy?.name})} + {translate('workspace.restrictedAction.actionsAreCurrentlyRestricted', {workspaceName: policy?.name ?? ''})} - {translate('workspace.restrictedAction.workspaceOwnerWillNeedToAddOrUpdatePaymentCard', {workspaceOwnerName: policy?.owner})} + {translate('workspace.restrictedAction.workspaceOwnerWillNeedToAddOrUpdatePaymentCard', {workspaceOwnerName: policy?.owner ?? ''})}