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 ?? ''})}
- {translate('workspace.restrictedAction.actionsAreCurrentlyRestricted', {workspaceName: policy?.name})}
+ {translate('workspace.restrictedAction.actionsAreCurrentlyRestricted', {workspaceName: policy?.name ?? ''})}
{translate('workspace.restrictedAction.pleaseReachOutToYourWorkspaceAdmin')}
diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx
index 87ee65a7e2df..c116a9dc5fbc 100644
--- a/src/pages/Search/AdvancedSearchFilters.tsx
+++ b/src/pages/Search/AdvancedSearchFilters.tsx
@@ -144,13 +144,13 @@ function getFilterDisplayTitle(filters: Partial, fiel
const {dateAfter, dateBefore} = filters;
let dateValue = '';
if (dateBefore) {
- dateValue = translate('search.filters.date.before', dateBefore);
+ dateValue = translate('search.filters.date.before', {date: dateBefore});
}
if (dateBefore && dateAfter) {
dateValue += ', ';
}
if (dateAfter) {
- dateValue += translate('search.filters.date.after', dateAfter);
+ dateValue += translate('search.filters.date.after', {date: dateAfter});
}
return dateValue;
@@ -159,13 +159,16 @@ function getFilterDisplayTitle(filters: Partial, fiel
if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT) {
const {lessThan, greaterThan} = filters;
if (lessThan && greaterThan) {
- return translate('search.filters.amount.between', convertToDisplayStringWithoutCurrency(Number(greaterThan)), convertToDisplayStringWithoutCurrency(Number(lessThan)));
+ return translate('search.filters.amount.between', {
+ lessThan: convertToDisplayStringWithoutCurrency(Number(lessThan)),
+ greaterThan: convertToDisplayStringWithoutCurrency(Number(greaterThan)),
+ });
}
if (lessThan) {
- return translate('search.filters.amount.lessThan', convertToDisplayStringWithoutCurrency(Number(lessThan)));
+ return translate('search.filters.amount.lessThan', {amount: convertToDisplayStringWithoutCurrency(Number(lessThan))});
}
if (greaterThan) {
- return translate('search.filters.amount.greaterThan', convertToDisplayStringWithoutCurrency(Number(greaterThan)));
+ return translate('search.filters.amount.greaterThan', {amount: convertToDisplayStringWithoutCurrency(Number(greaterThan))});
}
// Will never happen
return;
diff --git a/src/pages/Search/SavedSearchRenamePage.tsx b/src/pages/Search/SavedSearchRenamePage.tsx
index 2b227e581ac4..9e4d6122ea4d 100644
--- a/src/pages/Search/SavedSearchRenamePage.tsx
+++ b/src/pages/Search/SavedSearchRenamePage.tsx
@@ -28,6 +28,7 @@ function SavedSearchRenamePage({route}: {route: {params: {q: string; name: strin
Navigation.navigate(
ROUTES.SEARCH_CENTRAL_PANE.getRoute({
query: q,
+ name: newName,
}),
);
};
diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx
index 7095413398b8..684d45df5a34 100644
--- a/src/pages/Search/SearchPageBottomTab.tsx
+++ b/src/pages/Search/SearchPageBottomTab.tsx
@@ -28,6 +28,7 @@ function SearchPageBottomTab() {
const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE);
const searchParams = activeCentralPaneRoute?.params as AuthScreensParamList[typeof SCREENS.SEARCH.CENTRAL_PANE];
const parsedQuery = SearchUtils.buildSearchQueryJSON(searchParams?.q);
+ const searchName = searchParams?.name;
const policyIDFromSearchQuery = parsedQuery && SearchUtils.getPolicyIDFromSearchQuery(parsedQuery);
const isActiveCentralPaneRoute = activeCentralPaneRoute?.name === SCREENS.SEARCH.CENTRAL_PANE;
const queryJSON = isActiveCentralPaneRoute ? parsedQuery : undefined;
@@ -55,7 +56,10 @@ function SearchPageBottomTab() {
shouldDisplaySearchRouter={shouldUseNarrowLayout}
isCustomSearchQuery={shouldUseNarrowLayout && !SearchUtils.isCannedSearchQuery(queryJSON)}
/>
-
+
>
) : (
{
SearchActions.clearAllFilters();
- Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: item?.query ?? ''}));
+ Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: item?.query ?? '', name: item?.name}));
},
- rightComponent: ,
+ rightComponent: ,
styles: [styles.alignItemsCenter],
};
@@ -132,8 +133,8 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
tooltipShiftHorizontal: -32,
tooltipShiftVertical: 15,
tooltipWrapperStyle: [styles.bgPaleGreen, styles.mh4, styles.pv2],
+ onHideTooltip: () => SearchActions.dismissSavedSearchRenameTooltip(),
renderTooltipContent: () => {
- SearchActions.dismissSavedSearchRenameTooltip();
return (
),
@@ -202,7 +202,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
const activeItemIndex = isCannedQuery ? typeMenuItems.findIndex((item) => item.type === type) : -1;
if (shouldUseNarrowLayout) {
- const title = isCannedQuery ? undefined : SearchUtils.getSearchHeaderTitle(queryJSON, personalDetails, cardList, reports, taxRates);
+ const title = searchName ?? (isCannedQuery ? undefined : SearchUtils.getSearchHeaderTitle(queryJSON, personalDetails, cardList, reports, taxRates));
return (
);
})}
diff --git a/src/pages/WorkspaceSwitcherPage/WorkspacesSectionHeader.tsx b/src/pages/WorkspaceSwitcherPage/WorkspacesSectionHeader.tsx
index 18c3be9678c4..9c714e54704c 100644
--- a/src/pages/WorkspaceSwitcherPage/WorkspacesSectionHeader.tsx
+++ b/src/pages/WorkspaceSwitcherPage/WorkspacesSectionHeader.tsx
@@ -30,7 +30,7 @@ function WorkspacesSectionHeader() {
{
const activeRoute = Navigation.getActiveRouteWithoutParams();
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index e44a59baf7eb..69c4402e959b 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -694,6 +694,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro
const lastRoute = usePrevious(route);
const lastReportActionIDFromRoute = usePrevious(reportActionIDFromRoute);
+
// Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger.
// If we have cached reportActions, they will be shown immediately.
// We aim to display a loader first, then fetch relevant reportActions, and finally show them.
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
index 3248f400d1bc..371df4db2b3c 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
@@ -439,7 +439,7 @@ const ContextMenuActions: ContextMenuAction[] = [
setClipboardMessage(CONST.ACTIONABLE_TRACK_EXPENSE_WHISPER_MESSAGE);
} else if (ReportActionsUtils.isRenamedAction(reportAction)) {
setClipboardMessage(ReportActionsUtils.getRenamedAction(reportAction));
- } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) {
+ } else if (ReportActionsUtils.isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.SUBMITTED)) {
const displayMessage = ReportUtils.getIOUSubmittedMessage(reportAction);
Clipboard.setString(displayMessage);
} else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED) {
@@ -475,7 +475,7 @@ const ContextMenuActions: ContextMenuAction[] = [
setClipboardMessage(ReportActionsUtils.getPolicyChangeLogDeleteMemberMessage(reportAction));
} else if (ReportActionsUtils.isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.INTEGRATION_SYNC_FAILED)) {
const {label, errorMessage} = ReportActionsUtils.getOriginalMessage(reportAction) ?? {label: '', errorMessage: ''};
- setClipboardMessage(Localize.translateLocal('report.actions.type.integrationSyncFailed', label, errorMessage));
+ setClipboardMessage(Localize.translateLocal('report.actions.type.integrationSyncFailed', {label, errorMessage}));
} else if (ReportActionsUtils.isCardIssuedAction(reportAction)) {
setClipboardMessage(ReportActionsUtils.getCardIssuedMessage(reportAction, true));
} else if (content) {
diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
index 6e3c3a48de74..28d985537ff5 100644
--- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
+++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
@@ -1,8 +1,8 @@
import {useIsFocused} from '@react-navigation/native';
-import React, {useCallback, useEffect, useMemo} from 'react';
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import type {FileObject} from '@components/AttachmentModal';
import AttachmentPicker from '@components/AttachmentPicker';
import Icon from '@components/Icon';
@@ -25,20 +25,17 @@ import * as SubscriptionUtils from '@libs/SubscriptionUtils';
import * as IOU from '@userActions/IOU';
import * as Report from '@userActions/Report';
import * as Task from '@userActions/Task';
+import DelegateNoAccessModal from '@src/components/DelegateNoAccessModal';
import type {IOUType} from '@src/CONST';
import CONST from '@src/CONST';
+import useDelegateUserDetails from '@src/hooks/useDelegateUserDetails';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
type MoneyRequestOptions = Record, PopoverMenuItem>;
-type AttachmentPickerWithMenuItemsOnyxProps = {
- /** The policy tied to the report */
- policy: OnyxEntry;
-};
-
-type AttachmentPickerWithMenuItemsProps = AttachmentPickerWithMenuItemsOnyxProps & {
+type AttachmentPickerWithMenuItemsProps = {
/** The report currently being looked at */
report: OnyxEntry;
@@ -97,7 +94,6 @@ type AttachmentPickerWithMenuItemsProps = AttachmentPickerWithMenuItemsOnyxProps
*/
function AttachmentPickerWithMenuItems({
report,
- policy,
reportParticipantIDs,
displayFileInModal,
isFullComposerAvailable,
@@ -121,6 +117,9 @@ function AttachmentPickerWithMenuItems({
const {translate} = useLocalize();
const {windowHeight, windowWidth} = useWindowDimensions();
const {shouldUseNarrowLayout} = useResponsiveLayout();
+ const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails();
+ const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`);
/**
* Returns the list of IOU Options
@@ -149,7 +148,13 @@ function AttachmentPickerWithMenuItems({
[CONST.IOU.TYPE.PAY]: {
icon: getIconForAction(CONST.IOU.TYPE.SEND),
text: translate('iou.paySomeone', {name: ReportUtils.getPayeeName(report)}),
- onSelected: () => selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.PAY, report?.reportID ?? '-1'), false),
+ onSelected: () => {
+ if (isDelegateAccessRestricted) {
+ setIsNoDelegateAccessMenuVisible(true);
+ return;
+ }
+ selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.PAY, report?.reportID ?? '-1'), false);
+ },
},
[CONST.IOU.TYPE.TRACK]: {
icon: getIconForAction(CONST.IOU.TYPE.TRACK),
@@ -166,7 +171,7 @@ function AttachmentPickerWithMenuItems({
return ReportUtils.temporary_getMoneyRequestOptions(report, policy, reportParticipantIDs ?? []).map((option) => ({
...options[option],
}));
- }, [translate, report, policy, reportParticipantIDs]);
+ }, [translate, report, policy, reportParticipantIDs, isDelegateAccessRestricted]);
/**
* Determines if we can show the task option
@@ -327,6 +332,11 @@ function AttachmentPickerWithMenuItems({
withoutOverlay
anchorRef={actionButtonRef}
/>
+ setIsNoDelegateAccessMenuVisible(false)}
+ delegatorEmail={delegatorEmail ?? ''}
+ />
>
);
}}
@@ -336,9 +346,4 @@ function AttachmentPickerWithMenuItems({
AttachmentPickerWithMenuItems.displayName = 'AttachmentPickerWithMenuItems';
-export default withOnyx({
- policy: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`,
- initialValue: {} as OnyxTypes.Policy,
- },
-})(AttachmentPickerWithMenuItems);
+export default AttachmentPickerWithMenuItems;
diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
index 2bbf04ec3d2b..69bceec2bd9f 100644
--- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
@@ -4,7 +4,7 @@ import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 're
import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputFocusEventData, TextInputSelectionChangeEventData} from 'react-native';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import {runOnUI, useSharedValue} from 'react-native-reanimated';
import type {Emoji} from '@assets/emojis/types';
import type {FileObject} from '@components/AttachmentModal';
@@ -63,16 +63,7 @@ type SuggestionsRef = {
getIsSuggestionsMenuVisible: () => boolean;
};
-type ReportActionComposeOnyxProps = {
- /** The NVP describing a user's block status */
- blockedFromConcierge: OnyxEntry;
-
- /** Whether the composer input should be shown */
- shouldShowComposeInput: OnyxEntry;
-};
-
-type ReportActionComposeProps = ReportActionComposeOnyxProps &
- WithCurrentUserPersonalDetailsProps &
+type ReportActionComposeProps = WithCurrentUserPersonalDetailsProps &
Pick & {
/** A method to call when the form is submitted */
onSubmit: (newComment: string) => void;
@@ -109,7 +100,6 @@ const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc();
let onSubmitAction = noop;
function ReportActionCompose({
- blockedFromConcierge,
currentUserPersonalDetails,
disabled = false,
isComposerFullSize = false,
@@ -117,7 +107,6 @@ function ReportActionCompose({
pendingAction,
report,
reportID,
- shouldShowComposeInput = true,
isReportReadyForDisplay = true,
isEmptyChat,
lastReportAction,
@@ -133,7 +122,8 @@ function ReportActionCompose({
const actionButtonRef = useRef(null);
const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;
const navigation = useNavigation();
-
+ const [blockedFromConcierge] = useOnyx(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE);
+ const [shouldShowComposeInput = true] = useOnyx(ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT);
/**
* Updates the Highlight state of the composer
*/
@@ -425,7 +415,7 @@ function ReportActionCompose({
shouldRender={!shouldHideEducationalTooltip && shouldShowEducationalTooltip}
renderTooltipContent={renderWorkspaceChatTooltip}
shouldUseOverlay
- onPressOverlay={() => User.dismissWorkspaceTooltip()}
+ onHideTooltip={() => User.dismissWorkspaceTooltip()}
anchorAlignment={{
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
@@ -571,15 +561,6 @@ function ReportActionCompose({
ReportActionCompose.displayName = 'ReportActionCompose';
-export default withCurrentUserPersonalDetails(
- withOnyx({
- blockedFromConcierge: {
- key: ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE,
- },
- shouldShowComposeInput: {
- key: ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT,
- },
- })(memo(ReportActionCompose)),
-);
+export default withCurrentUserPersonalDetails(memo(ReportActionCompose));
export {onSubmitAction};
export type {SuggestionsRef, ComposerRef};
diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx
index 9c15427b0fda..8220362b6516 100644
--- a/src/pages/home/report/ReportActionItem.tsx
+++ b/src/pages/home/report/ReportActionItem.tsx
@@ -628,8 +628,17 @@ function ReportActionItem({
children = ;
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE) {
children = ;
- } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) {
- children = ;
+ } else if (ReportActionsUtils.isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.SUBMITTED)) {
+ const wasSubmittedViaHarvesting = ReportActionsUtils.getOriginalMessage(action)?.harvesting ?? false;
+ if (wasSubmittedViaHarvesting) {
+ children = (
+
+ ${ReportUtils.getReportAutomaticallySubmittedMessage(action)}`} />
+
+ );
+ } else {
+ children = ;
+ }
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED) {
children = ;
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.FORWARDED) {
@@ -674,7 +683,7 @@ function ReportActionItem({
children = ;
} else if (ReportActionsUtils.isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.INTEGRATION_SYNC_FAILED)) {
const {label, errorMessage} = ReportActionsUtils.getOriginalMessage(action) ?? {label: '', errorMessage: ''};
- children = ;
+ children = ;
} else {
const hasBeenFlagged =
![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === moderationDecision) &&
diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx
index 57ac3a0c0bf4..56eaf814ff10 100644
--- a/src/pages/home/report/ReportActionItemSingle.tsx
+++ b/src/pages/home/report/ReportActionItemSingle.tsx
@@ -20,6 +20,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import ControlSelection from '@libs/ControlSelection';
import DateUtils from '@libs/DateUtils';
import Navigation from '@libs/Navigation/Navigation';
+import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
import {getReportActionMessage} from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import CONST from '@src/CONST';
@@ -81,19 +82,22 @@ function ReportActionItemSingle({
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT;
- const actorAccountID = ReportUtils.getReportActionActorAccountID(action, iouReport);
+ const delegatePersonalDetails = personalDetails[action?.delegateAccountID ?? ''];
+ const actorAccountID = ReportUtils.getReportActionActorAccountID(action);
const [invoiceReceiverPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.invoiceReceiver && 'policyID' in report.invoiceReceiver ? report.invoiceReceiver.policyID : -1}`);
-
let displayName = ReportUtils.getDisplayNameForParticipant(actorAccountID);
+ const icons = ReportUtils.getIcons(iouReport ?? null, personalDetails);
const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {};
+ const accountOwnerDetails = getPersonalDetailByEmail(login ?? '');
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '');
const isTripRoom = ReportUtils.isTripRoom(report);
const isReportPreviewAction = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW;
- const displayAllActors = isReportPreviewAction && !isTripRoom;
+ const displayAllActors = isReportPreviewAction && !isTripRoom && ReportUtils.isIOUReport(iouReport ?? null) && icons.length > 1;
const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport ?? null);
const isWorkspaceActor = isInvoiceReport || (ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors));
const ownerAccountID = iouReport?.ownerAccountID ?? action?.childOwnerAccountID;
+ const managerID = iouReport?.managerID ?? action?.childManagerAccountID;
let avatarSource = avatar;
let avatarId: number | string | undefined = actorAccountID;
@@ -103,14 +107,9 @@ function ReportActionItemSingle({
avatarSource = ReportUtils.getWorkspaceIcon(report).source;
avatarId = report?.policyID;
} else if (action?.delegateAccountID && personalDetails[action?.delegateAccountID]) {
- // We replace the actor's email, name, and avatar with the Copilot manually for now. And only if we have their
- // details. This will be improved upon when the Copilot feature is implemented.
- const delegateDetails = personalDetails[action.delegateAccountID];
- const delegateDisplayName = delegateDetails?.displayName;
- actorHint = `${delegateDisplayName} (${translate('reportAction.asCopilot')} ${displayName})`;
- displayName = actorHint;
- avatarSource = delegateDetails?.avatar;
- avatarId = action.delegateAccountID;
+ displayName = delegatePersonalDetails?.displayName ?? '';
+ avatarSource = delegatePersonalDetails?.avatar;
+ avatarId = delegatePersonalDetails?.accountID;
} else if (isReportPreviewAction && isTripRoom) {
displayName = report?.reportName ?? '';
}
@@ -130,10 +129,9 @@ function ReportActionItemSingle({
};
} else {
// The ownerAccountID and actorAccountID can be the same if a user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice
- const secondaryAccountId = ownerAccountID === actorAccountID || isInvoiceReport ? actorAccountID : ownerAccountID;
+ const secondaryAccountId = ownerAccountID === actorAccountID || isInvoiceReport ? managerID : ownerAccountID;
const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? FallbackAvatar;
const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId);
-
secondaryAvatar = {
source: secondaryUserAvatar,
type: CONST.ICON_TYPE_AVATAR,
@@ -150,24 +148,41 @@ function ReportActionItemSingle({
} else {
secondaryAvatar = {name: '', source: '', type: 'avatar'};
}
- const icon = {
- source: avatarSource ?? FallbackAvatar,
- type: isWorkspaceActor ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR,
- name: primaryDisplayName ?? '',
- id: avatarId,
- };
+
+ const icon = useMemo(
+ () => ({
+ source: avatarSource ?? FallbackAvatar,
+ type: isWorkspaceActor ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR,
+ name: primaryDisplayName ?? '',
+ id: avatarId,
+ }),
+ [avatarSource, isWorkspaceActor, primaryDisplayName, avatarId],
+ );
// Since the display name for a report action message is delivered with the report history as an array of fragments
// we'll need to take the displayName from personal details and have it be in the same format for now. Eventually,
// we should stop referring to the report history items entirely for this information.
- const personArray = displayName
- ? [
- {
- type: 'TEXT',
- text: displayName,
- },
- ]
- : action?.person;
+ const personArray = useMemo(() => {
+ const baseArray = displayName
+ ? [
+ {
+ type: 'TEXT',
+ text: displayName,
+ },
+ ]
+ : action?.person ?? [];
+
+ if (displayAllActors) {
+ return [
+ ...baseArray,
+ {
+ type: 'TEXT',
+ text: secondaryAvatar.name ?? '',
+ },
+ ];
+ }
+ return baseArray;
+ }, [displayName, action?.person, displayAllActors, secondaryAvatar?.name]);
const reportID = report?.reportID;
const iouReportID = iouReport?.reportID;
@@ -192,45 +207,130 @@ function ReportActionItemSingle({
[action, isWorkspaceActor, actorAccountID],
);
- const getAvatar = () => {
- if (displayAllActors) {
+ const getAvatar = useMemo(() => {
+ return () => {
+ if (displayAllActors) {
+ return (
+
+ );
+ }
+ if (shouldShowSubscriptAvatar) {
+ return (
+
+ );
+ }
return (
-
+
+
+
+
+
);
- }
- if (shouldShowSubscriptAvatar) {
+ };
+ }, [
+ displayAllActors,
+ shouldShowSubscriptAvatar,
+ actorAccountID,
+ action?.delegateAccountID,
+ icon,
+ styles.actionAvatar,
+ fallbackIcon,
+ icons,
+ StyleUtils,
+ theme.appBG,
+ theme.hoverComponentBG,
+ theme.componentBG,
+ isHovered,
+ secondaryAvatar,
+ ]);
+
+ const getHeading = useMemo(() => {
+ return () => {
+ if (displayAllActors && secondaryAvatar.name && isReportPreviewAction) {
+ return (
+
+
+
+ {` & `}
+
+
+
+ );
+ }
return (
-
- );
- }
- return (
-
-
+ {personArray?.map((fragment) => (
+
+ ))}
-
- );
- };
+ );
+ };
+ }, [
+ displayAllActors,
+ secondaryAvatar,
+ isReportPreviewAction,
+ personArray,
+ styles.flexRow,
+ styles.flex1,
+ styles.chatItemMessageHeaderSender,
+ styles.pre,
+ action,
+ actorAccountID,
+ displayName,
+ icon,
+ ]);
+
const hasEmojiStatus = !displayAllActors && status?.emojiCode;
const formattedDate = DateUtils.getStatusUntilDate(status?.clearAfter ?? '');
const statusText = status?.text ?? '';
@@ -261,18 +361,7 @@ function ReportActionItemSingle({
accessibilityLabel={actorHint}
role={CONST.ROLE.BUTTON}
>
- {personArray?.map((fragment, index) => (
-
- ))}
+ {getHeading()}
{!!hasEmojiStatus && (
@@ -285,12 +374,13 @@ function ReportActionItemSingle({
) : null}
+ {action?.delegateAccountID && !isReportPreviewAction && (
+ {translate('delegate.onBehalfOfMessage', {delegator: accountOwnerDetails?.displayName ?? ''})}
+ )}
{children}
);
}
-
ReportActionItemSingle.displayName = 'ReportActionItemSingle';
-
export default ReportActionItemSingle;
diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx
index 9ff8762956d7..8f5fc907a962 100644
--- a/src/pages/home/report/ReportActionsList.tsx
+++ b/src/pages/home/report/ReportActionsList.tsx
@@ -268,7 +268,7 @@ function ReportActionsList({
}
const mostRecentReportActionCreated = sortedVisibleReportActions[0]?.created ?? '';
- if (mostRecentReportActionCreated === unreadMarkerTime) {
+ if (mostRecentReportActionCreated <= unreadMarkerTime) {
return;
}
diff --git a/src/pages/home/report/ReportActionsListItemRenderer.tsx b/src/pages/home/report/ReportActionsListItemRenderer.tsx
index ff1c2431ca8b..63b2cb43d836 100644
--- a/src/pages/home/report/ReportActionsListItemRenderer.tsx
+++ b/src/pages/home/report/ReportActionsListItemRenderer.tsx
@@ -171,7 +171,7 @@ function ReportActionsListItemRenderer({
displayAsGroup={displayAsGroup}
shouldDisplayNewMarker={shouldDisplayNewMarker}
shouldShowSubscriptAvatar={
- ReportUtils.isPolicyExpenseChat(report) &&
+ (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isInvoiceRoom(report)) &&
[
CONST.REPORT.ACTIONS.TYPE.IOU,
CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW,
diff --git a/src/pages/home/report/ReportDetailsExportPage.tsx b/src/pages/home/report/ReportDetailsExportPage.tsx
index bed7569cf888..0da9a7c1bb8e 100644
--- a/src/pages/home/report/ReportDetailsExportPage.tsx
+++ b/src/pages/home/report/ReportDetailsExportPage.tsx
@@ -57,7 +57,7 @@ function ReportDetailsExportPage({route}: ReportDetailsExportPageProps) {
const exportSelectorOptions: ExportSelectorType[] = [
{
value: CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION,
- text: translate('workspace.common.exportIntegrationSelected', connectionName),
+ text: translate('workspace.common.exportIntegrationSelected', {connectionName}),
icons: [
{
source: iconToDisplay ?? '',
@@ -124,7 +124,7 @@ function ReportDetailsExportPage({route}: ReportDetailsExportPageProps) {
title={translate('workspace.exportAgainModal.title')}
onConfirm={confirmExport}
onCancel={() => setModalStatus(null)}
- prompt={translate('workspace.exportAgainModal.description', report?.reportName ?? '', connectionName)}
+ prompt={translate('workspace.exportAgainModal.description', {reportName: report?.reportName ?? '', connectionName})}
confirmText={translate('workspace.exportAgainModal.confirmText')}
cancelText={translate('workspace.exportAgainModal.cancelText')}
isVisible={!!modalStatus}
diff --git a/src/pages/home/sidebar/AllSettingsScreen.tsx b/src/pages/home/sidebar/AllSettingsScreen.tsx
index c94c7012e411..0025e6b429c5 100644
--- a/src/pages/home/sidebar/AllSettingsScreen.tsx
+++ b/src/pages/home/sidebar/AllSettingsScreen.tsx
@@ -1,6 +1,5 @@
import React, {useMemo} from 'react';
-import type {OnyxCollection} from 'react-native-onyx';
-import {useOnyx, withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import Breadcrumbs from '@components/Breadcrumbs';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItemList from '@components/MenuItemList';
@@ -17,15 +16,10 @@ import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {Policy} from '@src/types/onyx';
-type AllSettingsScreenOnyxProps = {
- policies: OnyxCollection;
-};
+function AllSettingsScreen() {
+ const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
-type AllSettingsScreenProps = AllSettingsScreenOnyxProps;
-
-function AllSettingsScreen({policies}: AllSettingsScreenProps) {
const styles = useThemeStyles();
const waitForNavigate = useWaitForNavigation();
const {translate} = useLocalize();
@@ -86,7 +80,6 @@ function AllSettingsScreen({policies}: AllSettingsScreenProps) {
shouldShowRightIcon: item.shouldShowRightIcon,
shouldBlockSelection: !!item.link,
wrapperStyle: styles.sectionMenuItem,
- isPaneMenu: true,
focused: item.focused,
brickRoadIndicator: item.brickRoadIndicator,
}));
@@ -122,8 +115,4 @@ function AllSettingsScreen({policies}: AllSettingsScreenProps) {
AllSettingsScreen.displayName = 'AllSettingsScreen';
-export default withOnyx({
- policies: {
- key: ONYXKEYS.COLLECTION.POLICY,
- },
-})(AllSettingsScreen);
+export default AllSettingsScreen;
diff --git a/src/pages/home/sidebar/AvatarWithDelegateAvatar.tsx b/src/pages/home/sidebar/AvatarWithDelegateAvatar.tsx
index 0192d3d8423a..05f4c5aec343 100644
--- a/src/pages/home/sidebar/AvatarWithDelegateAvatar.tsx
+++ b/src/pages/home/sidebar/AvatarWithDelegateAvatar.tsx
@@ -9,7 +9,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ProfileAvatarWithIndicator from './ProfileAvatarWithIndicator';
type AvatarWithDelegateAvatarProps = {
- /** Emoji status */
+ /** Original account of delegate */
delegateEmail: string;
/** Whether the avatar is selected */
diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx
index 4444f519dbcd..8791a2fc7ec8 100644
--- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx
+++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx
@@ -215,7 +215,7 @@ function FloatingActionButtonAndPopover(
}
if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && quickActionAvatars.length > 0) {
const name: string = ReportUtils.getDisplayNameForParticipant(+(quickActionAvatars[0]?.id ?? -1), true) ?? '';
- return translate('quickAction.paySomeone', name);
+ return translate('quickAction.paySomeone', {name});
}
const titleKey = getQuickActionTitle(quickAction?.action ?? ('' as QuickActionName));
return titleKey ? translate(titleKey) : '';
diff --git a/src/pages/iou/HoldReasonPage.tsx b/src/pages/iou/HoldReasonPage.tsx
index 82f29acf7d10..7523a0932c42 100644
--- a/src/pages/iou/HoldReasonPage.tsx
+++ b/src/pages/iou/HoldReasonPage.tsx
@@ -24,6 +24,8 @@ type HoldReasonPageRouteParams = {
/** Link to previous page */
backTo: Route;
+
+ searchHash?: number;
};
type HoldReasonPageProps = {
@@ -34,7 +36,7 @@ type HoldReasonPageProps = {
function HoldReasonPage({route}: HoldReasonPageProps) {
const {translate} = useLocalize();
- const {transactionID, reportID, backTo} = route.params;
+ const {transactionID, reportID, backTo, searchHash} = route.params;
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID || -1}`);
@@ -51,7 +53,7 @@ function HoldReasonPage({route}: HoldReasonPageProps) {
return;
}
- IOU.putOnHold(transactionID, values.comment, reportID);
+ IOU.putOnHold(transactionID, values.comment, reportID, searchHash);
Navigation.navigate(backTo);
};
diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx
index a83819afdfb4..0168133154ee 100644
--- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx
+++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx
@@ -169,9 +169,10 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF
excludeLogins: CONST.EXPENSIFY_EMAILS,
maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW,
preferPolicyExpenseChat: isPaidGroupPolicy,
+ preferRecentExpenseReports: action === CONST.IOU.ACTION.CREATE,
});
return newOptions;
- }, [areOptionsInitialized, defaultOptions, debouncedSearchTerm, participants, isPaidGroupPolicy, canUseP2PDistanceRequests, iouRequestType, isCategorizeOrShareAction]);
+ }, [areOptionsInitialized, defaultOptions, debouncedSearchTerm, participants, isPaidGroupPolicy, canUseP2PDistanceRequests, iouRequestType, isCategorizeOrShareAction, action]);
/**
* Returns the sections needed for the OptionsSelector
diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx
index 851f8ca5441b..8dce84f8b470 100755
--- a/src/pages/settings/InitialSettingsPage.tsx
+++ b/src/pages/settings/InitialSettingsPage.tsx
@@ -3,8 +3,7 @@ import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, use
// eslint-disable-next-line no-restricted-imports
import type {GestureResponderEvent, ScrollView as RNScrollView, ScrollViewProps, StyleProp, ViewStyle} from 'react-native';
import {NativeModules, View} from 'react-native';
-import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
-import {useOnyx, withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import AccountSwitcher from '@components/AccountSwitcher';
import AccountSwitcherSkeletonView from '@components/AccountSwitcherSkeletonView';
@@ -45,32 +44,11 @@ import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
-import type * as OnyxTypes from '@src/types/onyx';
import type {Icon as TIcon} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type IconAsset from '@src/types/utils/IconAsset';
-type InitialSettingsPageOnyxProps = {
- /** The user's wallet account */
- userWallet: OnyxEntry;
-
- /** List of bank accounts */
- bankAccountList: OnyxEntry;
-
- /** List of user's cards */
- fundList: OnyxEntry;
-
- /** Information about the user accepting the terms for payments */
- walletTerms: OnyxEntry;
-
- /** Login list for the user that is signed in */
- loginList: OnyxEntry;
-
- /** The policies which the user has access to */
- policies: OnyxCollection;
-};
-
-type InitialSettingsPageProps = InitialSettingsPageOnyxProps & WithCurrentUserPersonalDetailsProps;
+type InitialSettingsPageProps = WithCurrentUserPersonalDetailsProps;
type MenuData = {
translationKey: TranslationPaths;
@@ -94,7 +72,14 @@ type MenuData = {
type Menu = {sectionStyle: StyleProp; sectionTranslationKey: TranslationPaths; items: MenuData[]};
-function InitialSettingsPage({userWallet, bankAccountList, fundList, walletTerms, loginList, currentUserPersonalDetails, policies}: InitialSettingsPageProps) {
+function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPageProps) {
+ const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET);
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
+ const [fundList] = useOnyx(ONYXKEYS.FUND_LIST);
+ const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS);
+ const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
+ const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
+
const network = useNetwork();
const theme = useTheme();
const styles = useThemeStyles();
@@ -342,7 +327,6 @@ function InitialSettingsPage({userWallet, bankAccountList, fundList, walletTerms
!!item.routeName &&
!!(activeCentralPaneRoute.name.toLowerCase().replaceAll('_', '') === item.routeName.toLowerCase().replaceAll('/', ''))
}
- isPaneMenu
iconRight={item.iconRight}
shouldShowRightIcon={item.shouldShowRightIcon}
/>
@@ -450,25 +434,4 @@ function InitialSettingsPage({userWallet, bankAccountList, fundList, walletTerms
InitialSettingsPage.displayName = 'InitialSettingsPage';
-export default withCurrentUserPersonalDetails(
- withOnyx({
- userWallet: {
- key: ONYXKEYS.USER_WALLET,
- },
- bankAccountList: {
- key: ONYXKEYS.BANK_ACCOUNT_LIST,
- },
- fundList: {
- key: ONYXKEYS.FUND_LIST,
- },
- walletTerms: {
- key: ONYXKEYS.WALLET_TERMS,
- },
- loginList: {
- key: ONYXKEYS.LOGIN_LIST,
- },
- policies: {
- key: ONYXKEYS.COLLECTION.POLICY,
- },
- })(InitialSettingsPage),
-);
+export default withCurrentUserPersonalDetails(InitialSettingsPage);
diff --git a/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx b/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx
index 8c8292b1f320..2c60aef482a8 100644
--- a/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx
+++ b/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx
@@ -68,9 +68,9 @@ function ConfirmDelegatePage({route}: ConfirmDelegatePageProps) {
interactive={false}
/>
Navigation.navigate(ROUTES.SETTINGS_DELEGATE_ROLE.getRoute(login, role))}
shouldShowRightIcon
/>
diff --git a/src/pages/settings/Security/AddDelegate/SelectDelegateRolePage.tsx b/src/pages/settings/Security/AddDelegate/SelectDelegateRolePage.tsx
index 551f5c2c223c..4270441775cd 100644
--- a/src/pages/settings/Security/AddDelegate/SelectDelegateRolePage.tsx
+++ b/src/pages/settings/Security/AddDelegate/SelectDelegateRolePage.tsx
@@ -23,8 +23,8 @@ function SelectDelegateRolePage({route}: SelectDelegateRolePageProps) {
const styles = useThemeStyles();
const roleOptions = Object.values(CONST.DELEGATE_ROLE).map((role) => ({
value: role,
- text: translate('delegate.role', role),
- alternateText: translate('delegate.roleDescription', role),
+ text: translate('delegate.role', {role}),
+ alternateText: translate('delegate.roleDescription', {role}),
isSelected: role === route.params.role,
keyForList: role,
}));
diff --git a/src/pages/settings/Security/SecuritySettingsPage.tsx b/src/pages/settings/Security/SecuritySettingsPage.tsx
index e54e7d873096..b23ac04d4972 100644
--- a/src/pages/settings/Security/SecuritySettingsPage.tsx
+++ b/src/pages/settings/Security/SecuritySettingsPage.tsx
@@ -17,7 +17,6 @@ import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
import {clearAddDelegateErrors} from '@libs/actions/Delegate';
@@ -36,7 +35,6 @@ function SecuritySettingsPage() {
const {translate} = useLocalize();
const waitForNavigate = useWaitForNavigation();
const {shouldUseNarrowLayout} = useResponsiveLayout();
- const theme = useTheme();
const {canUseNewDotCopilot} = usePermissions();
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const isActingAsDelegate = !!account?.delegatedAccess?.delegate ?? false;
@@ -94,7 +92,7 @@ function SecuritySettingsPage() {
return {
title: personalDetail?.displayName ?? formattedEmail,
description: personalDetail?.displayName ? formattedEmail : '',
- badgeText: translate('delegate.role', role),
+ badgeText: translate('delegate.role', {role}),
avatarID: personalDetail?.accountID ?? -1,
icon: personalDetail?.avatar ?? FallbackAvatar,
iconType: CONST.ICON_TYPE_AVATAR,
@@ -117,7 +115,7 @@ function SecuritySettingsPage() {
return {
title: personalDetail?.displayName ?? formattedEmail,
description: personalDetail?.displayName ? formattedEmail : '',
- badgeText: translate('delegate.role', role),
+ badgeText: translate('delegate.role', {role}),
avatarID: personalDetail?.accountID ?? -1,
icon: personalDetail?.avatar ?? FallbackAvatar,
iconType: CONST.ICON_TYPE_AVATAR,
@@ -189,7 +187,6 @@ function SecuritySettingsPage() {