From 211a21334084d1b4c831675323a80662846d396c Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Wed, 22 Apr 2026 09:58:34 +0000 Subject: [PATCH 01/31] Detect duplicate Plaid feeds using raw card feed data to prevent infinite spinner When adding a duplicate Plaid direct feed, the BankConnection component shows an infinite spinner because checkIfNewFeedConnected cannot detect the existing feed - it gets filtered out by getCombinedCardFeedsFromAllFeeds. Pass raw (unfiltered) feed keys from settings.companyCards to checkIfNewFeedConnected so it can detect the pre-existing Plaid feed in the backend data. Also add missing return in native BankConnection after isNewFeedConnected block to prevent unnecessary importPlaidAccounts call. Co-authored-by: Joel Davies --- src/libs/CardUtils.ts | 13 ++++++++----- .../companyCards/BankConnection/index.native.tsx | 8 +++++--- .../workspace/companyCards/BankConnection/index.tsx | 7 ++++--- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 40588f4ce85a..22378e43369c 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1000,18 +1000,21 @@ function getCardAssignmentStartDate(isEditing: boolean | undefined, existingStar return isEditing ? (existingStartDate ?? format(new Date(), CONST.DATE.FNS_FORMAT_STRING)) : format(new Date(), CONST.DATE.FNS_FORMAT_STRING); } -function checkIfNewFeedConnected(prevFeedsData: CombinedCardFeeds, currentFeedsData: CombinedCardFeeds, plaidBank?: string) { +function checkIfNewFeedConnected(prevFeedsData: CombinedCardFeeds, currentFeedsData: CombinedCardFeeds, plaidBank?: string, rawFeedKeys?: string[]) { const prevFeeds = Object.keys(prevFeedsData); const currentFeeds = Object.keys(currentFeedsData); + const expectedPlaidFeedName = plaidBank ? `${CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID}.${plaidBank}` : undefined; + const plaidBankFound = plaidBank && - currentFeeds.find((feed) => { - return splitCardFeedWithDomainID(feed as CompanyCardFeedWithDomainID)?.feedName === `${CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID}.${plaidBank}`; - }); + (currentFeeds.find((feed) => { + return splitCardFeedWithDomainID(feed as CompanyCardFeedWithDomainID)?.feedName === expectedPlaidFeedName; + }) || + rawFeedKeys?.some((key) => key === expectedPlaidFeedName)); return { - isNewFeedConnected: currentFeeds.length > prevFeeds.length || plaidBankFound, + isNewFeedConnected: currentFeeds.length > prevFeeds.length || !!plaidBankFound, newFeed: currentFeeds.find((feed) => !prevFeeds.includes(feed)) as CompanyCardFeedWithDomainID | undefined, }; } diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index db09f8aa7a0c..aa5aff3493dd 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -60,13 +60,14 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const plaidToken = addNewCard?.data?.publicToken ?? assignCard?.cardToAssign?.plaidAccessToken; const isPlaid = !!plaidToken; const url = getCompanyCardBankConnection(policyID, bankName); - const [cardFeeds] = useCardFeeds(policyID); + const [cardFeeds, , rawCardFeeds] = useCardFeeds(policyID); const [isConnectionCompleted, setConnectionCompleted] = useState(false); const prevFeedsData = usePrevious(cardFeeds); const isFeedExpired = feed ? isSelectedFeedExpired(cardFeeds?.[feed]) : false; + const rawFeedKeys = useMemo(() => Object.keys(rawCardFeeds?.settings?.companyCards ?? {}), [rawCardFeeds?.settings?.companyCards]); const {isNewFeedConnected, newFeed} = useMemo( - () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCard?.data?.plaidConnectedFeed), - [addNewCard?.data?.plaidConnectedFeed, cardFeeds, prevFeedsData], + () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCard?.data?.plaidConnectedFeed, rawFeedKeys), + [addNewCard?.data?.plaidConnectedFeed, cardFeeds, prevFeedsData, rawFeedKeys], ); const headerTitleAddCards = !backTo ? translate('workspace.companyCards.addCards') : undefined; const headerTitle = feed ? translate('workspace.companyCards.assignCard') : headerTitleAddCards; @@ -152,6 +153,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC } else { Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); } + return; } if (isPlaid) { onImportPlaidAccounts(); diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 91aea689d9ae..9c53176f324b 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -56,15 +56,16 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD); const {feed: bankNameFromRoute, backTo, policyID: policyIDFromRoute} = route?.params ?? {}; const policyID = policyIDFromProps ?? policyIDFromRoute; - const [cardFeeds] = useCardFeeds(policyID); + const [cardFeeds, , rawCardFeeds] = useCardFeeds(policyID); const prevFeedsData = usePrevious(cardFeeds); const illustrations = useMemoizedLazyIllustrations(['PendingBank']); const [shouldBlockWindowOpen, setShouldBlockWindowOpen] = useState(false); const selectedBank = addNewCard?.data?.selectedBank; const bankName = feed ? getBankName(getCompanyCardFeed(feed)) : (bankNameFromRoute ?? addNewCard?.data?.plaidConnectedFeed ?? selectedBank); + const rawFeedKeys = useMemo(() => Object.keys(rawCardFeeds?.settings?.companyCards ?? {}), [rawCardFeeds?.settings?.companyCards]); const {isNewFeedConnected, newFeed} = useMemo( - () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCard?.data?.plaidConnectedFeed), - [addNewCard?.data?.plaidConnectedFeed, cardFeeds, prevFeedsData], + () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCard?.data?.plaidConnectedFeed, rawFeedKeys), + [addNewCard?.data?.plaidConnectedFeed, cardFeeds, prevFeedsData, rawFeedKeys], ); const {isOffline} = useNetwork(); const plaidToken = addNewCard?.data?.publicToken ?? assignCard?.cardToAssign?.plaidAccessToken; From 1deaeb4afd6a73de495e47df9e5bad8090406d8f Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Wed, 22 Apr 2026 10:08:13 +0000 Subject: [PATCH 02/31] Fix ESLint errors: use nullish coalescing and ref instead of state in effect - CardUtils.ts: Replace `||` with `??` for nullish coalescing compliance - BankConnection/index.tsx: Convert shouldBlockWindowOpen from useState to useRef since it's only read inside the effect, avoiding the set-state-in-effect lint warning and unnecessary re-renders Co-authored-by: Joel Davies --- src/libs/CardUtils.ts | 2 +- .../workspace/companyCards/BankConnection/index.tsx | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 22378e43369c..75195f168d05 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1010,7 +1010,7 @@ function checkIfNewFeedConnected(prevFeedsData: CombinedCardFeeds, currentFeedsD plaidBank && (currentFeeds.find((feed) => { return splitCardFeedWithDomainID(feed as CompanyCardFeedWithDomainID)?.feedName === expectedPlaidFeedName; - }) || + }) ?? rawFeedKeys?.some((key) => key === expectedPlaidFeedName)); return { diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 9c53176f324b..096019e1d203 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import ActivityIndicator from '@components/ActivityIndicator'; import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; @@ -59,7 +59,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const [cardFeeds, , rawCardFeeds] = useCardFeeds(policyID); const prevFeedsData = usePrevious(cardFeeds); const illustrations = useMemoizedLazyIllustrations(['PendingBank']); - const [shouldBlockWindowOpen, setShouldBlockWindowOpen] = useState(false); + const shouldBlockWindowOpenRef = useRef(false); const selectedBank = addNewCard?.data?.selectedBank; const bankName = feed ? getBankName(getCompanyCardFeed(feed)) : (bankNameFromRoute ?? addNewCard?.data?.plaidConnectedFeed ?? selectedBank); const rawFeedKeys = useMemo(() => Object.keys(rawCardFeeds?.settings?.companyCards ?? {}), [rawCardFeeds?.settings?.companyCards]); @@ -151,7 +151,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC // Handle add new card flow if (isNewFeedConnected) { - setShouldBlockWindowOpen(true); + shouldBlockWindowOpenRef.current = true; customWindow?.close(); if (newFeed) { updateSelectedFeed(newFeed, policyID); @@ -169,7 +169,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC } return; } - if (!shouldBlockWindowOpen) { + if (!shouldBlockWindowOpenRef.current) { if (isPlaid) { onImportPlaidAccounts(); return; @@ -181,7 +181,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC }, [ isNewFeedConnected, isAllFeedsResultLoading, - shouldBlockWindowOpen, isBlockedToAddNewFeeds, newFeed, policyID, From a0b8d1b6185a19cbef3739a8faa1ce8e805c97f2 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Fri, 1 May 2026 09:29:13 +0000 Subject: [PATCH 03/31] Show ConfirmModal for duplicate Plaid card feeds instead of silently navigating When a user tries to add a Plaid card feed that already exists on the workspace, show a modal explaining the feed is already connected instead of silently closing the RHP. Co-authored-by: Joel Davies --- src/languages/de.ts | 5 ++++ src/languages/en.ts | 5 ++++ src/languages/es.ts | 5 ++++ src/languages/fr.ts | 5 ++++ src/languages/it.ts | 5 ++++ src/languages/ja.ts | 5 ++++ src/languages/nl.ts | 5 ++++ src/languages/pl.ts | 5 ++++ src/languages/pt-BR.ts | 5 ++++ src/languages/zh-hans.ts | 5 ++++ .../BankConnection/index.native.tsx | 21 ++++++++++++++++ .../companyCards/BankConnection/index.tsx | 25 ++++++++++++++++++- 12 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index df905666fd18..5a68aa994a77 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -5259,6 +5259,11 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU confirmText: 'Problem melden', cancelText: 'Überspringen', }, + duplicateFeedModal: { + title: 'Card feed already connected', + prompt: "You can't add the same card feed to the same workspace twice.", + confirmText: 'Got it', + }, csvColumns: { cardNumber: 'Kartennummer', postedDate: 'Datum', diff --git a/src/languages/en.ts b/src/languages/en.ts index 7cc597d4dd07..25702d0bcadb 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5262,6 +5262,11 @@ const translations = { confirmText: 'Report issue', cancelText: 'Skip', }, + duplicateFeedModal: { + title: 'Card feed already connected', + prompt: "You can't add the same card feed to the same workspace twice.", + confirmText: 'Got it', + }, csvColumns: { cardNumber: 'Card number', postedDate: 'Date', diff --git a/src/languages/es.ts b/src/languages/es.ts index 4679008524dc..86329070a48f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5103,6 +5103,11 @@ ${amount} para ${merchant} - ${date}`, confirmText: 'Informar problema', cancelText: 'Saltar', }, + duplicateFeedModal: { + title: 'Fuente de tarjetas ya conectada', + prompt: 'No puedes añadir la misma fuente de tarjetas al mismo espacio de trabajo dos veces.', + confirmText: 'Entendido', + }, csvColumns: { cardNumber: 'Número de la tarjeta', postedDate: 'Fecha', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 55231f1260d2..cb12690f0d1b 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5268,6 +5268,11 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. confirmText: 'Signaler un problème', cancelText: 'Ignorer', }, + duplicateFeedModal: { + title: 'Card feed already connected', + prompt: "You can't add the same card feed to the same workspace twice.", + confirmText: 'Got it', + }, csvColumns: { cardNumber: 'Numéro de carte', postedDate: 'Date', diff --git a/src/languages/it.ts b/src/languages/it.ts index a60fffd52a75..070fd02690df 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -5241,6 +5241,11 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. confirmText: 'Segnala problema', cancelText: 'Salta', }, + duplicateFeedModal: { + title: 'Card feed already connected', + prompt: "You can't add the same card feed to the same workspace twice.", + confirmText: 'Got it', + }, csvColumns: { cardNumber: 'Numero di carta', postedDate: 'Data', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 29018896654d..ab5fd42a65ff 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5192,6 +5192,11 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO confirmText: '問題を報告', cancelText: 'スキップ', }, + duplicateFeedModal: { + title: 'Card feed already connected', + prompt: "You can't add the same card feed to the same workspace twice.", + confirmText: 'Got it', + }, csvColumns: { cardNumber: 'カード番号', postedDate: '日付', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 526a70ae935d..243489921ec6 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -5232,6 +5232,11 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ confirmText: 'Probleem melden', cancelText: 'Overslaan', }, + duplicateFeedModal: { + title: 'Card feed already connected', + prompt: "You can't add the same card feed to the same workspace twice.", + confirmText: 'Got it', + }, csvColumns: { cardNumber: 'Kaartnummer', postedDate: 'Datum', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index ceb777e9f6eb..7fec4792ea5d 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -5223,6 +5223,11 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy confirmText: 'Zgłoś problem', cancelText: 'Pomiń', }, + duplicateFeedModal: { + title: 'Card feed already connected', + prompt: "You can't add the same card feed to the same workspace twice.", + confirmText: 'Got it', + }, csvColumns: { cardNumber: 'Numer karty', postedDate: 'Data', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 75efea94c433..74876f55ce00 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -5225,6 +5225,11 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS confirmText: 'Informar problema', cancelText: 'Pular', }, + duplicateFeedModal: { + title: 'Card feed already connected', + prompt: "You can't add the same card feed to the same workspace twice.", + confirmText: 'Got it', + }, csvColumns: { cardNumber: 'Número do cartão', postedDate: 'Data', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 754e202f1e87..5f9f03102542 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5108,6 +5108,11 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM confirmText: '报告问题', cancelText: '跳过', }, + duplicateFeedModal: { + title: 'Card feed already connected', + prompt: "You can't add the same card feed to the same workspace twice.", + confirmText: 'Got it', + }, csvColumns: { cardNumber: '卡号', postedDate: '日期', diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index aa5aff3493dd..1ca34dbd7634 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -4,6 +4,7 @@ import type {WebViewNavigation} from 'react-native-webview'; import {WebView} from 'react-native-webview'; import ActivityIndicator from '@components/ActivityIndicator'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; +import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useCardFeeds from '@hooks/useCardFeeds'; @@ -75,6 +76,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {updateBrokenConnection, isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); + const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid; + const [isDuplicateFeedDismissed, setIsDuplicateFeedDismissed] = useState(false); const activityReasonAttributes: SkeletonSpanReasonAttributes = { context: 'BankConnection', @@ -140,6 +143,11 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC // Handle add new card flow if (isNewFeedConnected) { + // Duplicate feed detected — modal is shown via render, skip navigation + if (isDuplicateFeed) { + return; + } + if (newFeed) { updateSelectedFeed(newFeed, policyID); } @@ -160,6 +168,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC } }, [ isNewFeedConnected, + isDuplicateFeed, newFeed, policyID, url, @@ -222,6 +231,18 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC /> )} + { + setIsDuplicateFeedDismissed(true); + Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); + }} + onCancel={() => setIsDuplicateFeedDismissed(true)} + /> ); } diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 096019e1d203..49de0d34222a 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -1,7 +1,8 @@ -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import ActivityIndicator from '@components/ActivityIndicator'; import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; +import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; @@ -79,6 +80,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const onImportPlaidAccounts = useImportPlaidAccounts(policyID); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); + const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid; + const [isDuplicateFeedDismissed, setIsDuplicateFeedDismissed] = useState(false); const onOpenBankConnectionFlow = useCallback(() => { if (!url) { @@ -153,6 +156,12 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC if (isNewFeedConnected) { shouldBlockWindowOpenRef.current = true; customWindow?.close(); + + // Duplicate feed detected — modal is shown via render, skip navigation + if (isDuplicateFeed) { + return; + } + if (newFeed) { updateSelectedFeed(newFeed, policyID); } @@ -182,6 +191,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isNewFeedConnected, isAllFeedsResultLoading, isBlockedToAddNewFeeds, + isDuplicateFeed, newFeed, policyID, url, @@ -243,6 +253,19 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC onBackButtonPress={handleBackButtonPress} /> {getContent()} + { + setIsDuplicateFeedDismissed(true); + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + }} + onCancel={() => setIsDuplicateFeedDismissed(true)} + /> ); } From 128c0d4dc842d840820673892ce429aea4f97d62 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Fri, 1 May 2026 11:11:15 +0000 Subject: [PATCH 04/31] Apply Polyglot Parrot translations for duplicateFeedModal Co-authored-by: Joel Davies --- src/languages/de.ts | 6 +++--- src/languages/fr.ts | 6 +++--- src/languages/it.ts | 6 +++--- src/languages/ja.ts | 6 +----- src/languages/nl.ts | 6 +----- src/languages/pl.ts | 6 +++--- src/languages/pt-BR.ts | 6 +----- src/languages/zh-hans.ts | 6 +----- 8 files changed, 16 insertions(+), 32 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 5a68aa994a77..0301499c37c2 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -5260,9 +5260,9 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU cancelText: 'Überspringen', }, duplicateFeedModal: { - title: 'Card feed already connected', - prompt: "You can't add the same card feed to the same workspace twice.", - confirmText: 'Got it', + title: 'Kartenfeed bereits verbunden', + prompt: 'Du kannst denselben Kartenfeed nicht zweimal zu demselben Workspace hinzufügen.', + confirmText: 'Verstanden', }, csvColumns: { cardNumber: 'Kartennummer', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index cb12690f0d1b..21325b0c2258 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5269,9 +5269,9 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. cancelText: 'Ignorer', }, duplicateFeedModal: { - title: 'Card feed already connected', - prompt: "You can't add the same card feed to the same workspace twice.", - confirmText: 'Got it', + title: 'Flux de carte déjà connecté', + prompt: 'Vous ne pouvez pas ajouter deux fois le même flux de carte au même espace de travail.', + confirmText: 'Compris', }, csvColumns: { cardNumber: 'Numéro de carte', diff --git a/src/languages/it.ts b/src/languages/it.ts index 070fd02690df..bdf54f8c7f2a 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -5242,9 +5242,9 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. cancelText: 'Salta', }, duplicateFeedModal: { - title: 'Card feed already connected', - prompt: "You can't add the same card feed to the same workspace twice.", - confirmText: 'Got it', + title: 'Feed carta già connesso', + prompt: 'Non puoi aggiungere due volte lo stesso flusso di carta allo stesso spazio di lavoro.', + confirmText: 'Ricevuto', }, csvColumns: { cardNumber: 'Numero di carta', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index ab5fd42a65ff..d6c4a9ab2618 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5192,11 +5192,7 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO confirmText: '問題を報告', cancelText: 'スキップ', }, - duplicateFeedModal: { - title: 'Card feed already connected', - prompt: "You can't add the same card feed to the same workspace twice.", - confirmText: 'Got it', - }, + duplicateFeedModal: {title: 'カードフィードは既に接続されています', prompt: '同じカードフィードを同じワークスペースに2回追加することはできません。', confirmText: '了解です'}, csvColumns: { cardNumber: 'カード番号', postedDate: '日付', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 243489921ec6..5c469a50179b 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -5232,11 +5232,7 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ confirmText: 'Probleem melden', cancelText: 'Overslaan', }, - duplicateFeedModal: { - title: 'Card feed already connected', - prompt: "You can't add the same card feed to the same workspace twice.", - confirmText: 'Got it', - }, + duplicateFeedModal: {title: 'Kaartfeed al verbonden', prompt: 'Je kunt dezelfde kaartfeed niet twee keer aan dezelfde workspace toevoegen.', confirmText: 'Begrepen'}, csvColumns: { cardNumber: 'Kaartnummer', postedDate: 'Datum', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 7fec4792ea5d..60dd210462b0 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -5224,9 +5224,9 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy cancelText: 'Pomiń', }, duplicateFeedModal: { - title: 'Card feed already connected', - prompt: "You can't add the same card feed to the same workspace twice.", - confirmText: 'Got it', + title: 'Kanał karty jest już połączony', + prompt: 'Nie możesz dodać tego samego źródła karty do tego samego obszaru roboczego dwukrotnie.', + confirmText: 'Rozumiem', }, csvColumns: { cardNumber: 'Numer karty', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 74876f55ce00..16195d15e252 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -5225,11 +5225,7 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS confirmText: 'Informar problema', cancelText: 'Pular', }, - duplicateFeedModal: { - title: 'Card feed already connected', - prompt: "You can't add the same card feed to the same workspace twice.", - confirmText: 'Got it', - }, + duplicateFeedModal: {title: 'Feed do cartão já conectado', prompt: 'Você não pode adicionar o mesmo feed de cartão ao mesmo workspace duas vezes.', confirmText: 'Entendido'}, csvColumns: { cardNumber: 'Número do cartão', postedDate: 'Data', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 5f9f03102542..5976f2c64e56 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5108,11 +5108,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM confirmText: '报告问题', cancelText: '跳过', }, - duplicateFeedModal: { - title: 'Card feed already connected', - prompt: "You can't add the same card feed to the same workspace twice.", - confirmText: 'Got it', - }, + duplicateFeedModal: {title: '卡片数据源已连接', prompt: '您不能将同一张卡的流水两次添加到同一个工作区。', confirmText: '明白了'}, csvColumns: { cardNumber: '卡号', postedDate: '日期', From cc9660f9a2323494f382ccb99e74d2436bf8ea5a Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Fri, 1 May 2026 11:17:36 +0000 Subject: [PATCH 05/31] Use common.buttonConfirm for duplicate feed modal confirm text Remove custom confirmText translation key from duplicateFeedModal and use the existing common.buttonConfirm translation instead. Co-authored-by: Joel Davies --- src/languages/de.ts | 1 - src/languages/en.ts | 1 - src/languages/es.ts | 1 - src/languages/fr.ts | 1 - src/languages/it.ts | 1 - src/languages/ja.ts | 2 +- src/languages/nl.ts | 2 +- src/languages/pl.ts | 1 - src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 2 +- .../workspace/companyCards/BankConnection/index.native.tsx | 2 +- src/pages/workspace/companyCards/BankConnection/index.tsx | 2 +- 12 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 0301499c37c2..2037a5c071b5 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -5262,7 +5262,6 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU duplicateFeedModal: { title: 'Kartenfeed bereits verbunden', prompt: 'Du kannst denselben Kartenfeed nicht zweimal zu demselben Workspace hinzufügen.', - confirmText: 'Verstanden', }, csvColumns: { cardNumber: 'Kartennummer', diff --git a/src/languages/en.ts b/src/languages/en.ts index 25702d0bcadb..a7346f65b4a7 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5265,7 +5265,6 @@ const translations = { duplicateFeedModal: { title: 'Card feed already connected', prompt: "You can't add the same card feed to the same workspace twice.", - confirmText: 'Got it', }, csvColumns: { cardNumber: 'Card number', diff --git a/src/languages/es.ts b/src/languages/es.ts index 86329070a48f..a000322dcab5 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5106,7 +5106,6 @@ ${amount} para ${merchant} - ${date}`, duplicateFeedModal: { title: 'Fuente de tarjetas ya conectada', prompt: 'No puedes añadir la misma fuente de tarjetas al mismo espacio de trabajo dos veces.', - confirmText: 'Entendido', }, csvColumns: { cardNumber: 'Número de la tarjeta', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 21325b0c2258..4ec772eaaa2e 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5271,7 +5271,6 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. duplicateFeedModal: { title: 'Flux de carte déjà connecté', prompt: 'Vous ne pouvez pas ajouter deux fois le même flux de carte au même espace de travail.', - confirmText: 'Compris', }, csvColumns: { cardNumber: 'Numéro de carte', diff --git a/src/languages/it.ts b/src/languages/it.ts index bdf54f8c7f2a..d08b1b9752d5 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -5244,7 +5244,6 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. duplicateFeedModal: { title: 'Feed carta già connesso', prompt: 'Non puoi aggiungere due volte lo stesso flusso di carta allo stesso spazio di lavoro.', - confirmText: 'Ricevuto', }, csvColumns: { cardNumber: 'Numero di carta', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index d6c4a9ab2618..49dd38857d19 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5192,7 +5192,7 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO confirmText: '問題を報告', cancelText: 'スキップ', }, - duplicateFeedModal: {title: 'カードフィードは既に接続されています', prompt: '同じカードフィードを同じワークスペースに2回追加することはできません。', confirmText: '了解です'}, + duplicateFeedModal: {title: 'カードフィードは既に接続されています', prompt: '同じカードフィードを同じワークスペースに2回追加することはできません。'}, csvColumns: { cardNumber: 'カード番号', postedDate: '日付', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 5c469a50179b..944c75b04d62 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -5232,7 +5232,7 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ confirmText: 'Probleem melden', cancelText: 'Overslaan', }, - duplicateFeedModal: {title: 'Kaartfeed al verbonden', prompt: 'Je kunt dezelfde kaartfeed niet twee keer aan dezelfde workspace toevoegen.', confirmText: 'Begrepen'}, + duplicateFeedModal: {title: 'Kaartfeed al verbonden', prompt: 'Je kunt dezelfde kaartfeed niet twee keer aan dezelfde workspace toevoegen.'}, csvColumns: { cardNumber: 'Kaartnummer', postedDate: 'Datum', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 60dd210462b0..66d74cae9bb0 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -5226,7 +5226,6 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy duplicateFeedModal: { title: 'Kanał karty jest już połączony', prompt: 'Nie możesz dodać tego samego źródła karty do tego samego obszaru roboczego dwukrotnie.', - confirmText: 'Rozumiem', }, csvColumns: { cardNumber: 'Numer karty', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 16195d15e252..487cb124ad20 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -5225,7 +5225,7 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS confirmText: 'Informar problema', cancelText: 'Pular', }, - duplicateFeedModal: {title: 'Feed do cartão já conectado', prompt: 'Você não pode adicionar o mesmo feed de cartão ao mesmo workspace duas vezes.', confirmText: 'Entendido'}, + duplicateFeedModal: {title: 'Feed do cartão já conectado', prompt: 'Você não pode adicionar o mesmo feed de cartão ao mesmo workspace duas vezes.'}, csvColumns: { cardNumber: 'Número do cartão', postedDate: 'Data', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 5976f2c64e56..1f5f26f1998c 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5108,7 +5108,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM confirmText: '报告问题', cancelText: '跳过', }, - duplicateFeedModal: {title: '卡片数据源已连接', prompt: '您不能将同一张卡的流水两次添加到同一个工作区。', confirmText: '明白了'}, + duplicateFeedModal: {title: '卡片数据源已连接', prompt: '您不能将同一张卡的流水两次添加到同一个工作区。'}, csvColumns: { cardNumber: '卡号', postedDate: '日期', diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 1ca34dbd7634..abaf0a13c890 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -235,7 +235,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isVisible={isDuplicateFeed && !isDuplicateFeedDismissed} title={translate('workspace.companyCards.addNewCard.duplicateFeedModal.title')} prompt={translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt')} - confirmText={translate('workspace.companyCards.addNewCard.duplicateFeedModal.confirmText')} + confirmText={translate('common.buttonConfirm')} shouldShowCancelButton={false} onConfirm={() => { setIsDuplicateFeedDismissed(true); diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 49de0d34222a..82eb120309a5 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -257,7 +257,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isVisible={isDuplicateFeed && !isDuplicateFeedDismissed} title={translate('workspace.companyCards.addNewCard.duplicateFeedModal.title')} prompt={translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt')} - confirmText={translate('workspace.companyCards.addNewCard.duplicateFeedModal.confirmText')} + confirmText={translate('common.buttonConfirm')} shouldShowCancelButton={false} onConfirm={() => { setIsDuplicateFeedDismissed(true); From 044a48f14ebb878a6d5f200106ed3e8be32b4313 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Fri, 1 May 2026 11:56:33 +0000 Subject: [PATCH 06/31] Fix duplicate feed modal briefly showing for new Plaid feeds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track whether onImportPlaidAccounts was called via isConfirmedNewFeed state. For genuinely new feeds, the import is called first (setting the flag), so the subsequent rawFeedKeys match is recognized as a new feed in transition — not a duplicate. For true duplicates, the plaid feed already exists in rawFeedKeys before any import is called, so isConfirmedNewFeed stays false and the modal correctly shows. Co-authored-by: Joel Davies --- .../workspace/companyCards/BankConnection/index.native.tsx | 5 ++++- src/pages/workspace/companyCards/BankConnection/index.tsx | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index abaf0a13c890..a3c22ae234ff 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -76,7 +76,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {updateBrokenConnection, isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); - const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid; + const [isConfirmedNewFeed, setIsConfirmedNewFeed] = useState(false); + const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid && !isConfirmedNewFeed; const [isDuplicateFeedDismissed, setIsDuplicateFeedDismissed] = useState(false); const activityReasonAttributes: SkeletonSpanReasonAttributes = { @@ -164,6 +165,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC return; } if (isPlaid) { + // eslint-disable-next-line react-hooks/set-state-in-effect -- marks the feed as new (not duplicate) before importing + setIsConfirmedNewFeed(true); onImportPlaidAccounts(); } }, [ diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 82eb120309a5..2fc327e6533f 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -80,7 +80,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const onImportPlaidAccounts = useImportPlaidAccounts(policyID); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); - const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid; + const [isConfirmedNewFeed, setIsConfirmedNewFeed] = useState(false); + const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid && !isConfirmedNewFeed; const [isDuplicateFeedDismissed, setIsDuplicateFeedDismissed] = useState(false); const onOpenBankConnectionFlow = useCallback(() => { @@ -180,6 +181,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC } if (!shouldBlockWindowOpenRef.current) { if (isPlaid) { + // eslint-disable-next-line react-hooks/set-state-in-effect -- marks the feed as new (not duplicate) before importing + setIsConfirmedNewFeed(true); onImportPlaidAccounts(); return; } From 7d9761000dc3a26faf6e795d12d68b2196fe19ad Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Fri, 1 May 2026 12:02:44 +0000 Subject: [PATCH 07/31] Move duplicate feed modal from BankConnection RHP to Company Cards page The ConfirmModal was previously rendered inside BankConnection, causing the spinner to show behind the modal while the RHP remained open. Now BankConnection sets an Onyx flag and navigates away immediately, and the WorkspaceCompanyCardsPage reads the flag to show the modal after the RHP has closed. Co-authored-by: Joel Davies --- .../BankConnection/index.native.tsx | 18 +++-------------- .../companyCards/BankConnection/index.tsx | 20 ++++--------------- .../WorkspaceCompanyCardsPage.tsx | 17 +++++++++++++++- src/types/onyx/CardFeeds.ts | 3 +++ 4 files changed, 26 insertions(+), 32 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index a3c22ae234ff..2db98bb3772a 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -4,7 +4,6 @@ import type {WebViewNavigation} from 'react-native-webview'; import {WebView} from 'react-native-webview'; import ActivityIndicator from '@components/ActivityIndicator'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; -import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useCardFeeds from '@hooks/useCardFeeds'; @@ -78,7 +77,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); const [isConfirmedNewFeed, setIsConfirmedNewFeed] = useState(false); const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid && !isConfirmedNewFeed; - const [isDuplicateFeedDismissed, setIsDuplicateFeedDismissed] = useState(false); const activityReasonAttributes: SkeletonSpanReasonAttributes = { context: 'BankConnection', @@ -144,8 +142,10 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC // Handle add new card flow if (isNewFeedConnected) { - // Duplicate feed detected — modal is shown via render, skip navigation + // Duplicate feed detected — navigate away and let the Company Cards page show the modal if (isDuplicateFeed) { + setAddNewCompanyCardStepAndData({data: {isDuplicateFeed: true}}); + Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); return; } @@ -234,18 +234,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC /> )} - { - setIsDuplicateFeedDismissed(true); - Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); - }} - onCancel={() => setIsDuplicateFeedDismissed(true)} - /> ); } diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 2fc327e6533f..6332c9ca71f4 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -2,7 +2,6 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import ActivityIndicator from '@components/ActivityIndicator'; import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; -import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; @@ -82,7 +81,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); const [isConfirmedNewFeed, setIsConfirmedNewFeed] = useState(false); const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid && !isConfirmedNewFeed; - const [isDuplicateFeedDismissed, setIsDuplicateFeedDismissed] = useState(false); const onOpenBankConnectionFlow = useCallback(() => { if (!url) { @@ -158,8 +156,11 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC shouldBlockWindowOpenRef.current = true; customWindow?.close(); - // Duplicate feed detected — modal is shown via render, skip navigation + // Duplicate feed detected — navigate away and let the Company Cards page show the modal if (isDuplicateFeed) { + setAddNewCompanyCardStepAndData({data: {isDuplicateFeed: true}}); + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); return; } @@ -256,19 +257,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC onBackButtonPress={handleBackButtonPress} /> {getContent()} - { - setIsDuplicateFeedDismissed(true); - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - }} - onCancel={() => setIsDuplicateFeedDismissed(true)} - /> ); } diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index fc3895d5336d..0c7651cc70bd 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -1,10 +1,12 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; +import ConfirmModal from '@components/ConfirmModal'; import DecisionModal from '@components/DecisionModal'; import useAssignCard from '@hooks/useAssignCard'; import useCompanyCards from '@hooks/useCompanyCards'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; @@ -14,8 +16,9 @@ import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import {getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; -import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage} from '@userActions/CompanyCards'; +import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage, setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import WorkspaceCompanyCardsTable from './WorkspaceCompanyCardsTable'; @@ -87,6 +90,8 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) { const [shouldShowOfflineModal, setShouldShowOfflineModal] = useState(false); const {assignCard, isAssigningCardDisabled} = useAssignCard({feedName, policyID, setShouldShowOfflineModal}); + const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD); + const isDuplicateFeed = addNewCard?.data?.isDuplicateFeed ?? false; return ( setShouldShowOfflineModal(false)} /> + + setAddNewCompanyCardStepAndData({data: {isDuplicateFeed: false}})} + onCancel={() => setAddNewCompanyCardStepAndData({data: {isDuplicateFeed: false}})} + /> ); } diff --git a/src/types/onyx/CardFeeds.ts b/src/types/onyx/CardFeeds.ts index d1075ef31a3a..e23ca999b40f 100644 --- a/src/types/onyx/CardFeeds.ts +++ b/src/types/onyx/CardFeeds.ts @@ -307,6 +307,9 @@ type AddNewCardFeedData = { /** Plaid accounts */ plaidAccounts?: LinkAccount[] | PlaidAccount[]; + + /** Whether a duplicate feed was detected during the add flow */ + isDuplicateFeed?: boolean; }; /** Issue new card flow steps */ From a228e3ddc0feb3d51116ae8df1a020de1cd28cc3 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Fri, 1 May 2026 13:00:36 +0000 Subject: [PATCH 08/31] Show duplicate feed modal in BankConnection instead of Company Cards page The modal was self-closing because AddNewCardPage's cleanup effect calls clearAddNewCardFlow() on unmount, which clears the isDuplicateFeed Onyx data before the Company Cards page can show the modal. Fix: Show the ConfirmModal directly in BankConnection using a latched state variable (set during render, never reset by derived value changes) so the modal stays visible until the user taps "Got it". Co-authored-by: Joel Davies --- src/languages/de.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 2 +- src/languages/nl.ts | 2 +- src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 2 +- .../BankConnection/index.native.tsx | 25 +++++++++++++---- .../companyCards/BankConnection/index.tsx | 27 ++++++++++++++----- .../WorkspaceCompanyCardsPage.tsx | 16 +---------- src/types/onyx/CardFeeds.ts | 3 --- 14 files changed, 52 insertions(+), 33 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 2037a5c071b5..6592e7783d32 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -5262,6 +5262,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU duplicateFeedModal: { title: 'Kartenfeed bereits verbunden', prompt: 'Du kannst denselben Kartenfeed nicht zweimal zu demselben Workspace hinzufügen.', + confirmText: 'Got it', }, csvColumns: { cardNumber: 'Kartennummer', diff --git a/src/languages/en.ts b/src/languages/en.ts index a7346f65b4a7..25702d0bcadb 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5265,6 +5265,7 @@ const translations = { duplicateFeedModal: { title: 'Card feed already connected', prompt: "You can't add the same card feed to the same workspace twice.", + confirmText: 'Got it', }, csvColumns: { cardNumber: 'Card number', diff --git a/src/languages/es.ts b/src/languages/es.ts index a000322dcab5..86329070a48f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5106,6 +5106,7 @@ ${amount} para ${merchant} - ${date}`, duplicateFeedModal: { title: 'Fuente de tarjetas ya conectada', prompt: 'No puedes añadir la misma fuente de tarjetas al mismo espacio de trabajo dos veces.', + confirmText: 'Entendido', }, csvColumns: { cardNumber: 'Número de la tarjeta', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 4ec772eaaa2e..790c079c476a 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5271,6 +5271,7 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. duplicateFeedModal: { title: 'Flux de carte déjà connecté', prompt: 'Vous ne pouvez pas ajouter deux fois le même flux de carte au même espace de travail.', + confirmText: 'Got it', }, csvColumns: { cardNumber: 'Numéro de carte', diff --git a/src/languages/it.ts b/src/languages/it.ts index d08b1b9752d5..93ab538664e8 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -5244,6 +5244,7 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. duplicateFeedModal: { title: 'Feed carta già connesso', prompt: 'Non puoi aggiungere due volte lo stesso flusso di carta allo stesso spazio di lavoro.', + confirmText: 'Got it', }, csvColumns: { cardNumber: 'Numero di carta', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 49dd38857d19..0d6935cb5b85 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5192,7 +5192,7 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO confirmText: '問題を報告', cancelText: 'スキップ', }, - duplicateFeedModal: {title: 'カードフィードは既に接続されています', prompt: '同じカードフィードを同じワークスペースに2回追加することはできません。'}, + duplicateFeedModal: {title: 'カードフィードは既に接続されています', prompt: '同じカードフィードを同じワークスペースに2回追加することはできません。', confirmText: 'Got it'}, csvColumns: { cardNumber: 'カード番号', postedDate: '日付', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 944c75b04d62..4a59d7340373 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -5232,7 +5232,7 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ confirmText: 'Probleem melden', cancelText: 'Overslaan', }, - duplicateFeedModal: {title: 'Kaartfeed al verbonden', prompt: 'Je kunt dezelfde kaartfeed niet twee keer aan dezelfde workspace toevoegen.'}, + duplicateFeedModal: {title: 'Kaartfeed al verbonden', prompt: 'Je kunt dezelfde kaartfeed niet twee keer aan dezelfde workspace toevoegen.', confirmText: 'Got it'}, csvColumns: { cardNumber: 'Kaartnummer', postedDate: 'Datum', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 66d74cae9bb0..d6b8ab708738 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -5226,6 +5226,7 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy duplicateFeedModal: { title: 'Kanał karty jest już połączony', prompt: 'Nie możesz dodać tego samego źródła karty do tego samego obszaru roboczego dwukrotnie.', + confirmText: 'Got it', }, csvColumns: { cardNumber: 'Numer karty', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 487cb124ad20..20b437ed1604 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -5225,7 +5225,7 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS confirmText: 'Informar problema', cancelText: 'Pular', }, - duplicateFeedModal: {title: 'Feed do cartão já conectado', prompt: 'Você não pode adicionar o mesmo feed de cartão ao mesmo workspace duas vezes.'}, + duplicateFeedModal: {title: 'Feed do cartão já conectado', prompt: 'Você não pode adicionar o mesmo feed de cartão ao mesmo workspace duas vezes.', confirmText: 'Got it'}, csvColumns: { cardNumber: 'Número do cartão', postedDate: 'Data', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 1f5f26f1998c..0f3048a2ab41 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5108,7 +5108,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM confirmText: '报告问题', cancelText: '跳过', }, - duplicateFeedModal: {title: '卡片数据源已连接', prompt: '您不能将同一张卡的流水两次添加到同一个工作区。'}, + duplicateFeedModal: {title: '卡片数据源已连接', prompt: '您不能将同一张卡的流水两次添加到同一个工作区。', confirmText: 'Got it'}, csvColumns: { cardNumber: '卡号', postedDate: '日期', diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 2db98bb3772a..ace0c4128100 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -4,6 +4,7 @@ import type {WebViewNavigation} from 'react-native-webview'; import {WebView} from 'react-native-webview'; import ActivityIndicator from '@components/ActivityIndicator'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; +import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useCardFeeds from '@hooks/useCardFeeds'; @@ -77,6 +78,10 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); const [isConfirmedNewFeed, setIsConfirmedNewFeed] = useState(false); const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid && !isConfirmedNewFeed; + const [showDuplicateFeedModal, setShowDuplicateFeedModal] = useState(false); + if (isDuplicateFeed && !showDuplicateFeedModal) { + setShowDuplicateFeedModal(true); + } const activityReasonAttributes: SkeletonSpanReasonAttributes = { context: 'BankConnection', @@ -142,10 +147,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC // Handle add new card flow if (isNewFeedConnected) { - // Duplicate feed detected — navigate away and let the Company Cards page show the modal - if (isDuplicateFeed) { - setAddNewCompanyCardStepAndData({data: {isDuplicateFeed: true}}); - Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); + // Duplicate feed detected — modal is shown via render, skip navigation + if (showDuplicateFeedModal) { return; } @@ -171,7 +174,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC } }, [ isNewFeedConnected, - isDuplicateFeed, + showDuplicateFeedModal, newFeed, policyID, url, @@ -234,6 +237,18 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC /> )} + { + setShowDuplicateFeedModal(false); + Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); + }} + onCancel={() => setShowDuplicateFeedModal(false)} + /> ); } diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 6332c9ca71f4..2fdaf628596e 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -2,6 +2,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import ActivityIndicator from '@components/ActivityIndicator'; import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; +import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; @@ -81,6 +82,10 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); const [isConfirmedNewFeed, setIsConfirmedNewFeed] = useState(false); const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid && !isConfirmedNewFeed; + const [showDuplicateFeedModal, setShowDuplicateFeedModal] = useState(false); + if (isDuplicateFeed && !showDuplicateFeedModal) { + setShowDuplicateFeedModal(true); + } const onOpenBankConnectionFlow = useCallback(() => { if (!url) { @@ -156,11 +161,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC shouldBlockWindowOpenRef.current = true; customWindow?.close(); - // Duplicate feed detected — navigate away and let the Company Cards page show the modal - if (isDuplicateFeed) { - setAddNewCompanyCardStepAndData({data: {isDuplicateFeed: true}}); - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + // Duplicate feed detected — modal is shown via render, skip navigation + if (showDuplicateFeedModal) { return; } @@ -195,7 +197,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isNewFeedConnected, isAllFeedsResultLoading, isBlockedToAddNewFeeds, - isDuplicateFeed, + showDuplicateFeedModal, newFeed, policyID, url, @@ -257,6 +259,19 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC onBackButtonPress={handleBackButtonPress} /> {getContent()} + { + setShowDuplicateFeedModal(false); + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + }} + onCancel={() => setShowDuplicateFeedModal(false)} + /> ); } diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 0c7651cc70bd..d48ae5569bc6 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -1,12 +1,10 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; -import ConfirmModal from '@components/ConfirmModal'; import DecisionModal from '@components/DecisionModal'; import useAssignCard from '@hooks/useAssignCard'; import useCompanyCards from '@hooks/useCompanyCards'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; @@ -16,9 +14,8 @@ import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import {getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; -import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage, setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards'; +import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import WorkspaceCompanyCardsTable from './WorkspaceCompanyCardsTable'; @@ -90,8 +87,6 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) { const [shouldShowOfflineModal, setShouldShowOfflineModal] = useState(false); const {assignCard, isAssigningCardDisabled} = useAssignCard({feedName, policyID, setShouldShowOfflineModal}); - const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD); - const isDuplicateFeed = addNewCard?.data?.isDuplicateFeed ?? false; return ( setShouldShowOfflineModal(false)} /> - setAddNewCompanyCardStepAndData({data: {isDuplicateFeed: false}})} - onCancel={() => setAddNewCompanyCardStepAndData({data: {isDuplicateFeed: false}})} - /> ); } diff --git a/src/types/onyx/CardFeeds.ts b/src/types/onyx/CardFeeds.ts index e23ca999b40f..d1075ef31a3a 100644 --- a/src/types/onyx/CardFeeds.ts +++ b/src/types/onyx/CardFeeds.ts @@ -307,9 +307,6 @@ type AddNewCardFeedData = { /** Plaid accounts */ plaidAccounts?: LinkAccount[] | PlaidAccount[]; - - /** Whether a duplicate feed was detected during the add flow */ - isDuplicateFeed?: boolean; }; /** Issue new card flow steps */ From 70b332b914637eb240380fcefb43b14b0b1f710c Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Fri, 1 May 2026 13:02:49 +0000 Subject: [PATCH 09/31] Remove seatbelt entry for BankConnection set-state-in-effect The violation is now suppressed via eslint-disable-next-line comment, so the seatbelt entry is no longer needed. Co-authored-by: Joel Davies --- config/eslint/eslint.seatbelt.tsv | 1 - 1 file changed, 1 deletion(-) diff --git a/config/eslint/eslint.seatbelt.tsv b/config/eslint/eslint.seatbelt.tsv index bbfc257f8685..6355d8a67ddf 100644 --- a/config/eslint/eslint.seatbelt.tsv +++ b/config/eslint/eslint.seatbelt.tsv @@ -462,7 +462,6 @@ "../../src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx" "rulesdir/no-default-id-values" 1 "../../src/pages/workspace/categories/CategorySettingsPage.tsx" "react-hooks/preserve-manual-memoization" 1 "../../src/pages/workspace/categories/WorkspaceCategoriesPage.tsx" "react-hooks/set-state-in-effect" 2 -"../../src/pages/workspace/companyCards/BankConnection/index.tsx" "react-hooks/set-state-in-effect" 1 "../../src/pages/workspace/companyCards/WorkspaceVerifyWorkAccountPage.tsx" "react-hooks/set-state-in-effect" 1 "../../src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx" "react-hooks/preserve-manual-memoization" 2 "../../src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx" "react-hooks/preserve-manual-memoization" 2 From c5e17618103fbc369558dadee96870cbf10b7f33 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Thu, 14 May 2026 13:48:18 +0000 Subject: [PATCH 10/31] Move duplicate feed modal from BankConnection to WorkspaceCompanyCardsPage The ConfirmModal for duplicate Plaid feeds was rendered inside BankConnection, which meant the RHP stayed open with a spinner behind the modal. Now BankConnection sets an Onyx flag and navigates away immediately, and WorkspaceCompanyCardsPage shows the modal after the RHP has closed. Co-authored-by: Joel Davies --- config/eslint/eslint.seatbelt.tsv | 1 - .../BankConnection/index.native.tsx | 28 ++++-------------- .../companyCards/BankConnection/index.tsx | 29 ++++--------------- .../WorkspaceCompanyCardsPage.tsx | 21 +++++++++++++- src/types/onyx/CardFeeds.ts | 3 ++ 5 files changed, 33 insertions(+), 49 deletions(-) diff --git a/config/eslint/eslint.seatbelt.tsv b/config/eslint/eslint.seatbelt.tsv index fbb42a3ad5b6..64eb3e763229 100644 --- a/config/eslint/eslint.seatbelt.tsv +++ b/config/eslint/eslint.seatbelt.tsv @@ -650,7 +650,6 @@ "../../src/pages/workspace/categories/CategorySettingsPage.tsx" "react-hooks/preserve-manual-memoization" 1 "../../src/pages/workspace/categories/WorkspaceCategoriesPage.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1 "../../src/pages/workspace/categories/WorkspaceCategoriesPage.tsx" "react-hooks/set-state-in-effect" 2 -"../../src/pages/workspace/companyCards/BankConnection/index.tsx" "react-hooks/set-state-in-effect" 1 "../../src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1 "../../src/pages/workspace/companyCards/WorkspaceVerifyWorkAccountPage.tsx" "react-hooks/set-state-in-effect" 1 "../../src/pages/workspace/companyCards/addNew/AddNewCardPage.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1 diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index e56b18c8afa4..dafb15c59b2a 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -4,7 +4,6 @@ import type {WebViewNavigation} from 'react-native-webview'; import {WebView} from 'react-native-webview'; import ActivityIndicator from '@components/ActivityIndicator'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; -import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useCardFeeds from '@hooks/useCardFeeds'; @@ -78,10 +77,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); const [isConfirmedNewFeed, setIsConfirmedNewFeed] = useState(false); const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid && !isConfirmedNewFeed; - const [showDuplicateFeedModal, setShowDuplicateFeedModal] = useState(false); - if (isDuplicateFeed && !showDuplicateFeedModal) { - setShowDuplicateFeedModal(true); - } const activityReasonAttributes: SkeletonSpanReasonAttributes = { context: 'BankConnection', @@ -147,15 +142,14 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC // Handle add new card flow if (isNewFeedConnected) { - // Duplicate feed detected — modal is shown via render, skip navigation - if (showDuplicateFeedModal) { - return; - } - if (newFeed) { updateSelectedFeed(newFeed, policyID); } + if (isDuplicateFeed) { + setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: true}}); + } + Navigation.closeRHPFlow(); Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); return; @@ -167,7 +161,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC } }, [ isNewFeedConnected, - showDuplicateFeedModal, + isDuplicateFeed, newFeed, policyID, url, @@ -230,18 +224,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC /> )} - { - setShowDuplicateFeedModal(false); - Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); - }} - onCancel={() => setShowDuplicateFeedModal(false)} - /> ); } diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index b7b0eea71737..f507cf2dc8b9 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -2,7 +2,6 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import ActivityIndicator from '@components/ActivityIndicator'; import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; -import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; @@ -82,10 +81,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); const [isConfirmedNewFeed, setIsConfirmedNewFeed] = useState(false); const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid && !isConfirmedNewFeed; - const [showDuplicateFeedModal, setShowDuplicateFeedModal] = useState(false); - if (isDuplicateFeed && !showDuplicateFeedModal) { - setShowDuplicateFeedModal(true); - } const onOpenBankConnectionFlow = useCallback(() => { if (!url) { @@ -161,15 +156,14 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC shouldBlockWindowOpenRef.current = true; customWindow?.close(); - // Duplicate feed detected — modal is shown via render, skip navigation - if (showDuplicateFeedModal) { - return; - } - if (newFeed) { updateSelectedFeed(newFeed, policyID); } + if (isDuplicateFeed) { + setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: true}}); + } + Navigation.closeRHPFlow(); Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); return; @@ -189,7 +183,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isNewFeedConnected, isAllFeedsResultLoading, isBlockedToAddNewFeeds, - showDuplicateFeedModal, + isDuplicateFeed, newFeed, policyID, url, @@ -251,19 +245,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC onBackButtonPress={handleBackButtonPress} /> {getContent()} - { - setShowDuplicateFeedModal(false); - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - }} - onCancel={() => setShowDuplicateFeedModal(false)} - /> ); } diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 6eec067f5f91..1e9791974dc0 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -3,9 +3,11 @@ import DecisionModal from '@components/DecisionModal'; import WorkspaceCompanyCardsTable from '@components/Tables/WorkspaceCompanyCardsTable'; import useAssignCard from '@hooks/useAssignCard'; import useCompanyCards from '@hooks/useCompanyCards'; +import useConfirmModal from '@hooks/useConfirmModal'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; @@ -15,8 +17,9 @@ import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import {getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; -import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage} from '@userActions/CompanyCards'; +import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage, setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; @@ -87,6 +90,22 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) { const [shouldShowOfflineModal, setShouldShowOfflineModal] = useState(false); const {assignCard, isAssigningCardDisabled} = useAssignCard({feedName, policyID, setShouldShowOfflineModal}); + const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD); + const isDuplicateFeedDetected = !!addNewCard?.data?.isDuplicateFeedDetected; + const {showConfirmModal} = useConfirmModal(); + + useEffect(() => { + if (!isDuplicateFeedDetected) { + return; + } + setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: false}}); + showConfirmModal({ + title: translate('workspace.companyCards.addNewCard.duplicateFeedModal.title'), + prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), + confirmText: translate('workspace.companyCards.addNewCard.duplicateFeedModal.confirmText'), + shouldShowCancelButton: false, + }); + }, [isDuplicateFeedDetected, showConfirmModal, translate]); return ( Date: Thu, 14 May 2026 15:49:18 +0000 Subject: [PATCH 11/31] Use common.buttonConfirm for duplicate feed modal Co-authored-by: Joel Davies --- src/languages/de.ts | 1 - src/languages/en.ts | 1 - src/languages/es.ts | 1 - src/languages/fr.ts | 1 - src/languages/it.ts | 1 - src/languages/ja.ts | 2 +- src/languages/nl.ts | 2 +- src/languages/pl.ts | 1 - src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 2 +- src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx | 2 +- 11 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 33785249f384..70273c25b277 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -5303,7 +5303,6 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU duplicateFeedModal: { title: 'Kartenfeed bereits verbunden', prompt: 'Du kannst denselben Kartenfeed nicht zweimal zu demselben Workspace hinzufügen.', - confirmText: 'Got it', }, csvColumns: { cardNumber: 'Kartennummer', diff --git a/src/languages/en.ts b/src/languages/en.ts index ecc467d1c99d..46f117a2d3c9 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5308,7 +5308,6 @@ const translations = { duplicateFeedModal: { title: 'Card feed already connected', prompt: "You can't add the same card feed to the same workspace twice.", - confirmText: 'Got it', }, csvColumns: { cardNumber: 'Card number', diff --git a/src/languages/es.ts b/src/languages/es.ts index 4f51402a2c88..ea500a7f5cd5 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5142,7 +5142,6 @@ ${amount} para ${merchant} - ${date}`, duplicateFeedModal: { title: 'Fuente de tarjetas ya conectada', prompt: 'No puedes añadir la misma fuente de tarjetas al mismo espacio de trabajo dos veces.', - confirmText: 'Entendido', }, csvColumns: { cardNumber: 'Número de la tarjeta', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 3ae77de77b10..5777b378b1f6 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5312,7 +5312,6 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. duplicateFeedModal: { title: 'Flux de carte déjà connecté', prompt: 'Vous ne pouvez pas ajouter deux fois le même flux de carte au même espace de travail.', - confirmText: 'Got it', }, csvColumns: { cardNumber: 'Numéro de carte', diff --git a/src/languages/it.ts b/src/languages/it.ts index 3331baa431e4..c14e2c6016c1 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -5286,7 +5286,6 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. duplicateFeedModal: { title: 'Feed carta già connesso', prompt: 'Non puoi aggiungere due volte lo stesso flusso di carta allo stesso spazio di lavoro.', - confirmText: 'Got it', }, csvColumns: { cardNumber: 'Numero di carta', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 96a9d672b4bb..5f7a40305b10 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5232,7 +5232,7 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO confirmText: '問題を報告', cancelText: 'スキップ', }, - duplicateFeedModal: {title: 'カードフィードは既に接続されています', prompt: '同じカードフィードを同じワークスペースに2回追加することはできません。', confirmText: 'Got it'}, + duplicateFeedModal: {title: 'カードフィードは既に接続されています', prompt: '同じカードフィードを同じワークスペースに2回追加することはできません。'}, csvColumns: { cardNumber: 'カード番号', postedDate: '日付', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 7c2bf8cd3279..15a597660b60 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -5271,7 +5271,7 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ confirmText: 'Probleem melden', cancelText: 'Overslaan', }, - duplicateFeedModal: {title: 'Kaartfeed al verbonden', prompt: 'Je kunt dezelfde kaartfeed niet twee keer aan dezelfde workspace toevoegen.', confirmText: 'Got it'}, + duplicateFeedModal: {title: 'Kaartfeed al verbonden', prompt: 'Je kunt dezelfde kaartfeed niet twee keer aan dezelfde workspace toevoegen.'}, csvColumns: { cardNumber: 'Kaartnummer', postedDate: 'Datum', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index abc86d2cbd4e..4af329567bff 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -5267,7 +5267,6 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy duplicateFeedModal: { title: 'Kanał karty jest już połączony', prompt: 'Nie możesz dodać tego samego źródła karty do tego samego obszaru roboczego dwukrotnie.', - confirmText: 'Got it', }, csvColumns: { cardNumber: 'Numer karty', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index d242d34599b8..90a9f703ee8c 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -5266,7 +5266,7 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS confirmText: 'Informar problema', cancelText: 'Pular', }, - duplicateFeedModal: {title: 'Feed do cartão já conectado', prompt: 'Você não pode adicionar o mesmo feed de cartão ao mesmo workspace duas vezes.', confirmText: 'Got it'}, + duplicateFeedModal: {title: 'Feed do cartão já conectado', prompt: 'Você não pode adicionar o mesmo feed de cartão ao mesmo workspace duas vezes.'}, csvColumns: { cardNumber: 'Número do cartão', postedDate: 'Data', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 46703be18d65..827d222cf778 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5148,7 +5148,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM confirmText: '报告问题', cancelText: '跳过', }, - duplicateFeedModal: {title: '卡片数据源已连接', prompt: '您不能将同一张卡的流水两次添加到同一个工作区。', confirmText: 'Got it'}, + duplicateFeedModal: {title: '卡片数据源已连接', prompt: '您不能将同一张卡的流水两次添加到同一个工作区。'}, csvColumns: { cardNumber: '卡号', postedDate: '日期', diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 1e9791974dc0..9e99445eaf20 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -102,7 +102,7 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) { showConfirmModal({ title: translate('workspace.companyCards.addNewCard.duplicateFeedModal.title'), prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), - confirmText: translate('workspace.companyCards.addNewCard.duplicateFeedModal.confirmText'), + confirmText: translate('common.buttonConfirm'), shouldShowCancelButton: false, }); }, [isDuplicateFeedDetected, showConfirmModal, translate]); From 9b386866b3921c5b6cd1b9af3669dbbf4b31dca1 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Fri, 15 May 2026 08:58:21 +0000 Subject: [PATCH 12/31] Show duplicate feed modal inside BankConnection RHP instead of after navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, detecting a duplicate Plaid feed would immediately close the RHP and navigate to the company cards page, then show the modal there — causing a visible flash. Now the confirm modal is shown while still in the BankConnection RHP, and navigation only occurs after the user taps confirm. Co-authored-by: Joel Davies --- .../BankConnection/index.native.tsx | 21 +++++++++++++++---- .../companyCards/BankConnection/index.tsx | 21 +++++++++++++++---- .../WorkspaceCompanyCardsPage.tsx | 21 +------------------ src/types/onyx/CardFeeds.ts | 3 --- 4 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index dafb15c59b2a..b704eb1586b1 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -7,6 +7,7 @@ import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOffli import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useCardFeeds from '@hooks/useCardFeeds'; +import useConfirmModal from '@hooks/useConfirmModal'; import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import useLocalize from '@hooks/useLocalize'; @@ -72,6 +73,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const headerTitleAddCards = !backTo ? translate('workspace.companyCards.addCards') : undefined; const headerTitle = feed ? translate('workspace.companyCards.assignCard') : headerTitleAddCards; const onImportPlaidAccounts = useImportPlaidAccounts(policyID); + const {showConfirmModal} = useConfirmModal(); const {updateBrokenConnection, isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); @@ -142,12 +144,21 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC // Handle add new card flow if (isNewFeedConnected) { - if (newFeed) { - updateSelectedFeed(newFeed, policyID); + if (isDuplicateFeed) { + showConfirmModal({ + title: translate('workspace.companyCards.addNewCard.duplicateFeedModal.title'), + prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + }).then(() => { + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + }); + return; } - if (isDuplicateFeed) { - setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: true}}); + if (newFeed) { + updateSelectedFeed(newFeed, policyID); } Navigation.closeRHPFlow(); @@ -173,6 +184,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isFeedConnectionBroken, updateBrokenConnection, isNewFeedHasError, + showConfirmModal, + translate, ]); const checkIfConnectionCompleted = (navState: WebViewNavigation) => { diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index f507cf2dc8b9..e7915fdd9f14 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -7,6 +7,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useCardFeeds from '@hooks/useCardFeeds'; +import useConfirmModal from '@hooks/useConfirmModal'; import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; @@ -70,6 +71,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {isOffline} = useNetwork(); const plaidToken = addNewCard?.data?.publicToken ?? assignCard?.cardToAssign?.plaidAccessToken; const {updateBrokenConnection, isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); + const {showConfirmModal} = useConfirmModal(); const isPlaid = !!plaidToken; const url = getCompanyCardBankConnection(policyID, bankName); @@ -156,12 +158,21 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC shouldBlockWindowOpenRef.current = true; customWindow?.close(); - if (newFeed) { - updateSelectedFeed(newFeed, policyID); + if (isDuplicateFeed) { + showConfirmModal({ + title: translate('workspace.companyCards.addNewCard.duplicateFeedModal.title'), + prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + }).then(() => { + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + }); + return; } - if (isDuplicateFeed) { - setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: true}}); + if (newFeed) { + updateSelectedFeed(newFeed, policyID); } Navigation.closeRHPFlow(); @@ -196,6 +207,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isFeedConnectionBroken, updateBrokenConnection, isNewFeedHasError, + showConfirmModal, + translate, ]); const getContent = () => { diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 9e99445eaf20..6eec067f5f91 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -3,11 +3,9 @@ import DecisionModal from '@components/DecisionModal'; import WorkspaceCompanyCardsTable from '@components/Tables/WorkspaceCompanyCardsTable'; import useAssignCard from '@hooks/useAssignCard'; import useCompanyCards from '@hooks/useCompanyCards'; -import useConfirmModal from '@hooks/useConfirmModal'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; @@ -17,9 +15,8 @@ import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import {getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; -import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage, setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards'; +import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; @@ -90,22 +87,6 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) { const [shouldShowOfflineModal, setShouldShowOfflineModal] = useState(false); const {assignCard, isAssigningCardDisabled} = useAssignCard({feedName, policyID, setShouldShowOfflineModal}); - const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD); - const isDuplicateFeedDetected = !!addNewCard?.data?.isDuplicateFeedDetected; - const {showConfirmModal} = useConfirmModal(); - - useEffect(() => { - if (!isDuplicateFeedDetected) { - return; - } - setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: false}}); - showConfirmModal({ - title: translate('workspace.companyCards.addNewCard.duplicateFeedModal.title'), - prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - }); - }, [isDuplicateFeedDetected, showConfirmModal, translate]); return ( Date: Fri, 15 May 2026 14:44:28 +0000 Subject: [PATCH 13/31] Show duplicate feed info in BankConnection RHP with submit, then modal after close When a duplicate Plaid feed is detected, BankConnection now renders a ConfirmationPage inside the RHP with the error message and a confirm button instead of immediately navigating away or showing a modal overlay. When the user taps the button, the RHP closes and then the error modal appears on the WorkspaceCompanyCardsPage via the isDuplicateFeedDetected Onyx flag. Co-authored-by: Joel Davies --- .../BankConnection/index.native.tsx | 34 +++++++++++-------- .../companyCards/BankConnection/index.tsx | 32 ++++++++++------- .../WorkspaceCompanyCardsPage.tsx | 21 +++++++++++- src/types/onyx/CardFeeds.ts | 3 ++ 4 files changed, 61 insertions(+), 29 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index b704eb1586b1..349b1c1cf6c5 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -4,10 +4,10 @@ import type {WebViewNavigation} from 'react-native-webview'; import {WebView} from 'react-native-webview'; import ActivityIndicator from '@components/ActivityIndicator'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; +import ConfirmationPage from '@components/ConfirmationPage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useCardFeeds from '@hooks/useCardFeeds'; -import useConfirmModal from '@hooks/useConfirmModal'; import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import useLocalize from '@hooks/useLocalize'; @@ -73,7 +73,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const headerTitleAddCards = !backTo ? translate('workspace.companyCards.addCards') : undefined; const headerTitle = feed ? translate('workspace.companyCards.assignCard') : headerTitleAddCards; const onImportPlaidAccounts = useImportPlaidAccounts(policyID); - const {showConfirmModal} = useConfirmModal(); const {updateBrokenConnection, isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); @@ -145,15 +144,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC // Handle add new card flow if (isNewFeedConnected) { if (isDuplicateFeed) { - showConfirmModal({ - title: translate('workspace.companyCards.addNewCard.duplicateFeedModal.title'), - prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - }).then(() => { - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - }); return; } @@ -184,8 +174,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isFeedConnectionBroken, updateBrokenConnection, isNewFeedHasError, - showConfirmModal, - translate, ]); const checkIfConnectionCompleted = (navState: WebViewNavigation) => { @@ -195,6 +183,12 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC setConnectionCompleted(true); }; + const handleDuplicateFeedSubmit = () => { + setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: true}}); + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + }; + return ( - {!!url && !isConnectionCompleted && !isPlaid && !isNewFeedHasError && !isAllFeedsResultLoading && (!isBlockedToAddNewFeeds || !!feed) && ( + {isDuplicateFeed && ( + + )} + {!!url && !isDuplicateFeed && !isConnectionCompleted && !isPlaid && !isNewFeedHasError && !isAllFeedsResultLoading && (!isBlockedToAddNewFeeds || !!feed) && ( )} - {(isAllFeedsResultLoading || (isBlockedToAddNewFeeds && !feed) || isConnectionCompleted || isPlaid) && !isNewFeedHasError && ( + {!isDuplicateFeed && (isAllFeedsResultLoading || (isBlockedToAddNewFeeds && !feed) || isConnectionCompleted || isPlaid) && !isNewFeedHasError && ( { - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - }); return; } @@ -207,11 +197,27 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isFeedConnectionBroken, updateBrokenConnection, isNewFeedHasError, - showConfirmModal, - translate, ]); + const handleDuplicateFeedSubmit = () => { + setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: true}}); + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + }; + const getContent = () => { + if (isDuplicateFeed) { + return ( + + ); + } if (isNewFeedHasError) { return ( { + if (!isDuplicateFeedDetected) { + return; + } + setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: false}}); + showConfirmModal({ + title: translate('workspace.companyCards.addNewCard.duplicateFeedModal.title'), + prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + }); + }, [isDuplicateFeedDetected, showConfirmModal, translate]); return ( Date: Tue, 19 May 2026 15:09:40 +0000 Subject: [PATCH 14/31] Remove ConfirmationPage from BankConnection, navigate directly to modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a duplicate Plaid feed is detected, immediately close the RHP and navigate to WorkspaceCompanyCardsPage where the duplicate feed modal displays — instead of showing an intermediate ConfirmationPage with the same message first. Co-authored-by: Joel Davies --- .../BankConnection/index.native.tsx | 29 ++++--------------- .../companyCards/BankConnection/index.tsx | 29 ++++--------------- 2 files changed, 11 insertions(+), 47 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 349b1c1cf6c5..5fc6a08e12f5 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -4,7 +4,6 @@ import type {WebViewNavigation} from 'react-native-webview'; import {WebView} from 'react-native-webview'; import ActivityIndicator from '@components/ActivityIndicator'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; -import ConfirmationPage from '@components/ConfirmationPage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useCardFeeds from '@hooks/useCardFeeds'; @@ -76,8 +75,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {updateBrokenConnection, isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); - const [isConfirmedNewFeed, setIsConfirmedNewFeed] = useState(false); - const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid && !isConfirmedNewFeed; + const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid; const activityReasonAttributes: SkeletonSpanReasonAttributes = { context: 'BankConnection', @@ -144,6 +142,9 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC // Handle add new card flow if (isNewFeedConnected) { if (isDuplicateFeed) { + setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: true}}); + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); return; } @@ -156,8 +157,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC return; } if (isPlaid) { - // eslint-disable-next-line react-hooks/set-state-in-effect -- marks the feed as new (not duplicate) before importing - setIsConfirmedNewFeed(true); onImportPlaidAccounts(); } }, [ @@ -183,12 +182,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC setConnectionCompleted(true); }; - const handleDuplicateFeedSubmit = () => { - setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: true}}); - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - }; - return ( - {isDuplicateFeed && ( - - )} - {!!url && !isDuplicateFeed && !isConnectionCompleted && !isPlaid && !isNewFeedHasError && !isAllFeedsResultLoading && (!isBlockedToAddNewFeeds || !!feed) && ( + {!!url && !isConnectionCompleted && !isPlaid && !isNewFeedHasError && !isAllFeedsResultLoading && (!isBlockedToAddNewFeeds || !!feed) && ( )} - {!isDuplicateFeed && (isAllFeedsResultLoading || (isBlockedToAddNewFeeds && !feed) || isConnectionCompleted || isPlaid) && !isNewFeedHasError && ( + {(isAllFeedsResultLoading || (isBlockedToAddNewFeeds && !feed) || isConnectionCompleted || isPlaid) && !isNewFeedHasError && ( { if (!url) { @@ -158,6 +156,9 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC customWindow?.close(); if (isDuplicateFeed) { + setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: true}}); + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); return; } @@ -171,8 +172,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC } if (!shouldBlockWindowOpenRef.current) { if (isPlaid) { - // eslint-disable-next-line react-hooks/set-state-in-effect -- marks the feed as new (not duplicate) before importing - setIsConfirmedNewFeed(true); onImportPlaidAccounts(); return; } @@ -199,25 +198,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isNewFeedHasError, ]); - const handleDuplicateFeedSubmit = () => { - setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: true}}); - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - }; - const getContent = () => { - if (isDuplicateFeed) { - return ( - - ); - } if (isNewFeedHasError) { return ( Date: Wed, 20 May 2026 15:01:09 +0000 Subject: [PATCH 15/31] Show duplicate feed modal inline instead of via programmatic showConfirmModal Switch from useConfirmModal/showConfirmModal (programmatic global modal) to an inline ConfirmModal component controlled by isDuplicateFeedDetected state. This ensures the modal renders reliably after navigation transitions when the RHP auto-closes. Co-authored-by: Joel Davies --- .../WorkspaceCompanyCardsPage.tsx | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 9e99445eaf20..e0cc0d567942 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -1,9 +1,9 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; +import ConfirmModal from '@components/ConfirmModal'; import DecisionModal from '@components/DecisionModal'; import WorkspaceCompanyCardsTable from '@components/Tables/WorkspaceCompanyCardsTable'; import useAssignCard from '@hooks/useAssignCard'; import useCompanyCards from '@hooks/useCompanyCards'; -import useConfirmModal from '@hooks/useConfirmModal'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -92,20 +92,10 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) { const {assignCard, isAssigningCardDisabled} = useAssignCard({feedName, policyID, setShouldShowOfflineModal}); const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD); const isDuplicateFeedDetected = !!addNewCard?.data?.isDuplicateFeedDetected; - const {showConfirmModal} = useConfirmModal(); - useEffect(() => { - if (!isDuplicateFeedDetected) { - return; - } + const closeDuplicateFeedModal = useCallback(() => { setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: false}}); - showConfirmModal({ - title: translate('workspace.companyCards.addNewCard.duplicateFeedModal.title'), - prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - }); - }, [isDuplicateFeedDetected, showConfirmModal, translate]); + }, []); return ( setShouldShowOfflineModal(false)} /> + + ); } From 53ed2662b72960c3f59a93057665603fa3ae4469 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Wed, 20 May 2026 15:34:30 +0000 Subject: [PATCH 16/31] Revert all previous changes to start fresh Reverts all PR-specific changes back to the main branch state so we can reimagine the approach for detecting and handling duplicate Plaid feeds. Co-authored-by: Joel Davies --- config/eslint/eslint.seatbelt.tsv | 1 - src/languages/de.ts | 8 ++---- src/languages/en.ts | 8 ++---- src/languages/es.ts | 8 ++---- src/languages/fr.ts | 8 ++---- src/languages/it.ts | 8 ++---- src/languages/ja.ts | 5 ++-- src/languages/nl.ts | 5 ++-- src/languages/pl.ts | 8 ++---- src/languages/pt-BR.ts | 5 ++-- src/languages/zh-hans.ts | 5 ++-- src/libs/CardUtils.ts | 13 ++++------ .../BankConnection/index.native.tsx | 16 +++--------- .../companyCards/BankConnection/index.tsx | 26 ++++++------------- .../WorkspaceCompanyCardsPage.tsx | 21 +-------------- src/types/onyx/CardFeeds.ts | 3 --- 16 files changed, 37 insertions(+), 111 deletions(-) diff --git a/config/eslint/eslint.seatbelt.tsv b/config/eslint/eslint.seatbelt.tsv index 989f4aa537f0..14e9d6f93fe9 100644 --- a/config/eslint/eslint.seatbelt.tsv +++ b/config/eslint/eslint.seatbelt.tsv @@ -638,7 +638,6 @@ "../../src/pages/workspace/categories/CategorySettingsPage.tsx" "react-hooks/preserve-manual-memoization" 1 "../../src/pages/workspace/categories/WorkspaceCategoriesPage.tsx" "react-hooks/set-state-in-effect" 2 "../../src/pages/workspace/companyCards/BankConnection/index.tsx" "react-hooks/set-state-in-effect" 1 -"../../src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1 "../../src/pages/workspace/companyCards/WorkspaceVerifyWorkAccountPage.tsx" "react-hooks/set-state-in-effect" 1 "../../src/pages/workspace/companyCards/addNew/AddNewCardPage.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1 "../../src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1 diff --git a/src/languages/de.ts b/src/languages/de.ts index 4b833173bfcb..b22f6407f8a1 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -5331,10 +5331,6 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU confirmText: 'Problem melden', cancelText: 'Überspringen', }, - duplicateFeedModal: { - title: 'Kartenfeed bereits verbunden', - prompt: 'Du kannst denselben Kartenfeed nicht zweimal zu demselben Workspace hinzufügen.', - }, csvColumns: { cardNumber: 'Kartennummer', postedDate: 'Datum', @@ -7072,8 +7068,8 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc description: 'Für Mitarbeiter, die Ausgaben bei ihrem Arbeitgeber einreichen möchten.', }, }, - description: 'Wähle ein passendes Abo für dich. Eine detaillierte Liste der Funktionen und Preise findest du in unserem', - subscriptionLink: 'Hilfeseite zu Plantypen und Preisen', + description: 'Wählen Sie das passende Abo für Sie.', + subscriptionLink: 'Mehr erfahren', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `Sie haben sich bis zum Ende Ihres Jahresabonnements am ${annualSubscriptionEndDate} zu 1 aktivem Mitglied im Control-Tarif verpflichtet. Sie können ab dem ${annualSubscriptionEndDate} zu einem nutzungsbasierten Abonnement wechseln und in den Collect-Tarif herabstufen, indem Sie die automatische Verlängerung in`, other: `Sie haben sich bis zum Ende Ihres Jahresabonnements am ${annualSubscriptionEndDate} zu ${count} aktiven Mitgliedern im Control-Tarif verpflichtet. Ab dem ${annualSubscriptionEndDate} können Sie durch Deaktivieren der automatischen Verlängerung in ein nutzungsabhängiges Abonnement wechseln und auf den Collect-Tarif herabstufen in`, diff --git a/src/languages/en.ts b/src/languages/en.ts index c883abf3cd7e..162917a09e3a 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5346,10 +5346,6 @@ const translations = { confirmText: 'Report issue', cancelText: 'Skip', }, - duplicateFeedModal: { - title: 'Card feed already connected', - prompt: "You can't add the same card feed to the same workspace twice.", - }, csvColumns: { cardNumber: 'Card number', postedDate: 'Date', @@ -7173,8 +7169,8 @@ const translations = { description: 'For employees looking to submit expenses to their employer.', }, }, - description: "Choose a plan that's right for you. For a detailed list of features and pricing, check out our", - subscriptionLink: 'plan types and pricing help page', + description: "Choose a plan that's right for you.", + subscriptionLink: 'Learn more', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `You've committed to 1 active member on the Control plan until your annual subscription ends on ${annualSubscriptionEndDate}. You can switch to pay-per-use subscription and downgrade to the Collect plan starting ${annualSubscriptionEndDate} by disabling auto-renew in`, other: `You've committed to ${count} active members on the Control plan until your annual subscription ends on ${annualSubscriptionEndDate}. You can switch to pay-per-use subscription and downgrade to the Collect plan starting ${annualSubscriptionEndDate} by disabling auto-renew in`, diff --git a/src/languages/es.ts b/src/languages/es.ts index b4d41affb184..bd7ecc2ce80d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5157,10 +5157,6 @@ ${amount} para ${merchant} - ${date}`, confirmText: 'Informar problema', cancelText: 'Saltar', }, - duplicateFeedModal: { - title: 'Fuente de tarjetas ya conectada', - prompt: 'No puedes añadir la misma fuente de tarjetas al mismo espacio de trabajo dos veces.', - }, csvColumns: { cardNumber: 'Número de la tarjeta', postedDate: 'Fecha', @@ -6527,8 +6523,8 @@ ${amount} para ${merchant} - ${date}`, description: 'Para empleados que buscan enviar gastos a su empleador.', }, }, - description: 'Elige el plan adecuado para ti. Para ver una lista detallada de funciones y precios, consulta nuestra', - subscriptionLink: 'página de ayuda sobre tipos de planes y precios', + description: 'Elige el plan adecuado para ti.', + subscriptionLink: 'Más información', lockedPlanDescription: ({count, annualSubscriptionEndDate}) => ({ one: `Tienes un compromiso anual de 1 miembro activo en el plan Controlar hasta el ${annualSubscriptionEndDate}. Puedes cambiar a una suscripción de pago por uso y desmejorar al plan Recopilar a partir del ${annualSubscriptionEndDate} desactivando la renovación automática en`, other: `Tienes un compromiso anual de ${count} miembros activos en el plan Controlar hasta el ${annualSubscriptionEndDate}. Puedes cambiar a una suscripción de pago por uso y desmejorar al plan Recopilar a partir del ${annualSubscriptionEndDate} desactivando la renovación automática en`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 05840554d11f..afa06290d04e 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5340,10 +5340,6 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. confirmText: 'Signaler un problème', cancelText: 'Ignorer', }, - duplicateFeedModal: { - title: 'Flux de carte déjà connecté', - prompt: 'Vous ne pouvez pas ajouter deux fois le même flux de carte au même espace de travail.', - }, csvColumns: { cardNumber: 'Numéro de carte', postedDate: 'Date', @@ -7094,8 +7090,8 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e description: 'Pour les employés souhaitant soumettre des dépenses à leur employeur.', }, }, - description: 'Choisissez l’offre qui vous convient. Pour une liste détaillée des fonctionnalités et des tarifs, consultez notre', - subscriptionLink: "page d'aide sur les types de forfaits et les tarifs", + description: 'Choisissez l’offre qui vous convient.', + subscriptionLink: 'En savoir plus', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `Vous vous êtes engagé à 1 membre actif sur le plan Control jusqu'à la fin de votre abonnement annuel, le ${annualSubscriptionEndDate}. Vous pourrez passer à un abonnement à l’usage et rétrograder vers le plan Collect à partir du ${annualSubscriptionEndDate} en désactivant le renouvellement automatique dans`, other: `Vous vous êtes engagé·e à avoir ${count} membres actifs sur le forfait Control jusqu’à la fin de votre abonnement annuel le ${annualSubscriptionEndDate}. Vous pouvez passer à un abonnement à l’usage et rétrograder vers le forfait Collect à partir du ${annualSubscriptionEndDate} en désactivant le renouvellement automatique dans`, diff --git a/src/languages/it.ts b/src/languages/it.ts index c3832f07f17c..0c1d9780c449 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -5314,10 +5314,6 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. confirmText: 'Segnala problema', cancelText: 'Salta', }, - duplicateFeedModal: { - title: 'Feed carta già connesso', - prompt: 'Non puoi aggiungere due volte lo stesso flusso di carta allo stesso spazio di lavoro.', - }, csvColumns: { cardNumber: 'Numero di carta', postedDate: 'Data', @@ -7058,8 +7054,8 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, description: 'Per i dipendenti che desiderano inviare le spese al proprio datore di lavoro.', }, }, - description: 'Scegli il piano più adatto a te. Per un elenco dettagliato di funzionalità e prezzi, consulta la nostra', - subscriptionLink: 'pagina di aiuto su tipi di piano e prezzi', + description: 'Scegli il piano più adatto a te.', + subscriptionLink: 'Scopri di più', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `Ti sei impegnato per 1 membro attivo nel piano Control fino al termine dell’abbonamento annuale, il ${annualSubscriptionEndDate}. Puoi passare all’abbonamento a consumo e effettuare il downgrade al piano Collect a partire dal ${annualSubscriptionEndDate} disattivando il rinnovo automatico in`, other: `Ti sei impegnato per ${count} membri attivi nel piano Control fino alla fine dell’abbonamento annuale, il ${annualSubscriptionEndDate}. Puoi passare all’abbonamento a consumo e effettuare il downgrade al piano Collect a partire dal ${annualSubscriptionEndDate} disattivando il rinnovo automatico in`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index fa05c73ee058..7f1c803423dc 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5264,7 +5264,6 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO confirmText: '問題を報告', cancelText: 'スキップ', }, - duplicateFeedModal: {title: 'カードフィードは既に接続されています', prompt: '同じカードフィードを同じワークスペースに2回追加することはできません。'}, csvColumns: { cardNumber: 'カード番号', postedDate: '日付', @@ -6977,8 +6976,8 @@ ${reportName} description: '雇用主に経費を提出したい従業員向け。', }, }, - description: '自分に合ったプランをお選びください。機能と料金の詳細な一覧は、こちらのページをご覧ください', - subscriptionLink: 'プランの種類と料金のヘルプページ', + description: '自分に合ったプランをお選びください。', + subscriptionLink: '詳しく見る', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `${annualSubscriptionEndDate} までの年間サブスクリプション期間中、Control プランでアクティブメンバー 1 名を利用することに同意しています。${annualSubscriptionEndDate} 以降、 自動更新を無効にすることで、従量課金サブスクリプションに切り替え、Collect プランへダウングレードできます。`, other: `あなたは、年間サブスクリプションが${annualSubscriptionEndDate}に終了するまで、Controlプランでアクティブメンバー${count}名を契約しています。${annualSubscriptionEndDate}以降は、自動更新を無効にすることで、従量課金制サブスクリプションに切り替え、Collectプランへダウングレードできます。その操作は、`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index d371681485d3..d2166cd9b64b 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -5303,7 +5303,6 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ confirmText: 'Probleem melden', cancelText: 'Overslaan', }, - duplicateFeedModal: {title: 'Kaartfeed al verbonden', prompt: 'Je kunt dezelfde kaartfeed niet twee keer aan dezelfde workspace toevoegen.'}, csvColumns: { cardNumber: 'Kaartnummer', postedDate: 'Datum', @@ -7032,8 +7031,8 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, description: 'Voor werknemers die onkosten bij hun werkgever willen indienen.', }, }, - description: 'Kies een pakket dat bij je past. Voor een gedetailleerd overzicht van functies en prijzen, bekijk onze', - subscriptionLink: 'hulppagina voor abonnementstypen en prijzen', + description: 'Kies een abonnement dat bij je past.', + subscriptionLink: 'Meer informatie', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `Je hebt je vastgelegd op 1 actief lid in het Control-abonnement tot je jaarlijkse abonnement afloopt op ${annualSubscriptionEndDate}. Je kunt overstappen op een pay-per-use-abonnement en downgraden naar het Collect-abonnement vanaf ${annualSubscriptionEndDate} door automatisch verlengen uit te schakelen in`, other: `Je hebt je vastgelegd op ${count} actieve leden op het Control-abonnement totdat je jaarlijkse abonnement eindigt op ${annualSubscriptionEndDate}. Je kunt overschakelen naar een betaling-per-gebruik-abonnement en downgraden naar het Collect-abonnement vanaf ${annualSubscriptionEndDate} door automatisch verlengen uit te schakelen in`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 795f2897af68..99dbf05b915e 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -5295,10 +5295,6 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy confirmText: 'Zgłoś problem', cancelText: 'Pomiń', }, - duplicateFeedModal: { - title: 'Kanał karty jest już połączony', - prompt: 'Nie możesz dodać tego samego źródła karty do tego samego obszaru roboczego dwukrotnie.', - }, csvColumns: { cardNumber: 'Numer karty', postedDate: 'Data', @@ -7029,8 +7025,8 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, description: 'Dla pracowników, którzy chcą przesyłać wydatki do pracodawcy.', }, }, - description: 'Wybierz plan odpowiedni dla siebie. Szczegółową listę funkcji i cen znajdziesz w naszej', - subscriptionLink: 'strona pomocy dotycząca typów planów i cen', + description: 'Wybierz plan odpowiedni dla siebie.', + subscriptionLink: 'Dowiedz się więcej', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `Zobowiązałeś(-aś) się do 1 aktywnego członka w planie Control do końca rocznej subskrypcji ${annualSubscriptionEndDate}. Możesz przejść na subskrypcję z rozliczaniem za użycie i zmienić plan na Collect od ${annualSubscriptionEndDate}, wyłączając automatyczne odnawianie w`, other: `Zobowiązałeś(-aś) się do ${count} aktywnych członków w planie Control do końca rocznej subskrypcji ${annualSubscriptionEndDate}. Możesz przejść na subskrypcję płatną za użycie i zmienić plan na Collect od ${annualSubscriptionEndDate}, wyłączając automatyczne odnawianie w`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index f60fa6717f84..7fa52f68cbd1 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -5297,7 +5297,6 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS confirmText: 'Informar problema', cancelText: 'Pular', }, - duplicateFeedModal: {title: 'Feed do cartão já conectado', prompt: 'Você não pode adicionar o mesmo feed de cartão ao mesmo workspace duas vezes.'}, csvColumns: { cardNumber: 'Número do cartão', postedDate: 'Data', @@ -7032,8 +7031,8 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, description: 'Para funcionários que desejam enviar despesas ao empregador.', }, }, - description: 'Escolha o plano ideal para você. Para ver a lista detalhada de recursos e preços, confira nosso', - subscriptionLink: 'página de ajuda sobre tipos de plano e preços', + description: 'Escolha o plano ideal para você.', + subscriptionLink: 'Saiba mais', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `Você se comprometeu com 1 membro ativo no plano Control até o fim da sua assinatura anual em ${annualSubscriptionEndDate}. Você pode mudar para a assinatura pré-paga por uso e fazer downgrade para o plano Collect a partir de ${annualSubscriptionEndDate}, desativando a renovação automática em`, other: `Você se comprometeu com ${count} membros ativos no plano Control até o fim da sua assinatura anual em ${annualSubscriptionEndDate}. Você pode mudar para a assinatura pós-paga e fazer downgrade para o plano Collect a partir de ${annualSubscriptionEndDate} desativando a renovação automática em`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 1b6a46410fec..7281eaf7b8d6 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5179,7 +5179,6 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM confirmText: '报告问题', cancelText: '跳过', }, - duplicateFeedModal: {title: '卡片数据源已连接', prompt: '您不能将同一张卡的流水两次添加到同一个工作区。'}, csvColumns: { cardNumber: '卡号', postedDate: '日期', @@ -6857,8 +6856,8 @@ ${reportName} description: '适用于希望向雇主提交费用的员工。', }, }, - description: '选择适合您的方案。要查看详细的功能和价格列表,请访问我们的', - subscriptionLink: '方案类型和价格帮助页面', + description: '选择适合你的方案。', + subscriptionLink: '了解详情', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `在 Control 方案中,你已承诺在年度订阅到期(${annualSubscriptionEndDate})前保持 1 位活跃成员。你可以在 ${annualSubscriptionEndDate} 起关闭自动续订,以改为按使用付费订阅并降级到 Collect 方案,操作入口在`, other: `在年费订阅于${annualSubscriptionEndDate}结束之前,你已承诺在 Control 方案中保留 ${count} 名活跃成员。你可以在${annualSubscriptionEndDate}起,通过关闭自动续订,改为按使用量付费订阅并降级到 Collect 方案,操作入口在`, diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index daa80fa9a890..272308aadcc1 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1031,21 +1031,18 @@ function getCardAssignmentStartDate(isEditing: boolean | undefined, existingStar return isEditing ? (existingStartDate ?? format(new Date(), CONST.DATE.FNS_FORMAT_STRING)) : format(new Date(), CONST.DATE.FNS_FORMAT_STRING); } -function checkIfNewFeedConnected(prevFeedsData: CombinedCardFeeds, currentFeedsData: CombinedCardFeeds, plaidBank?: string, rawFeedKeys?: string[]) { +function checkIfNewFeedConnected(prevFeedsData: CombinedCardFeeds, currentFeedsData: CombinedCardFeeds, plaidBank?: string) { const prevFeeds = Object.keys(prevFeedsData); const currentFeeds = Object.keys(currentFeedsData); - const expectedPlaidFeedName = plaidBank ? `${CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID}.${plaidBank}` : undefined; - const plaidBankFound = plaidBank && - (currentFeeds.find((feed) => { - return splitCardFeedWithDomainID(feed as CompanyCardFeedWithDomainID)?.feedName === expectedPlaidFeedName; - }) ?? - rawFeedKeys?.some((key) => key === expectedPlaidFeedName)); + currentFeeds.find((feed) => { + return splitCardFeedWithDomainID(feed as CompanyCardFeedWithDomainID)?.feedName === `${CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID}.${plaidBank}`; + }); return { - isNewFeedConnected: currentFeeds.length > prevFeeds.length || !!plaidBankFound, + isNewFeedConnected: currentFeeds.length > prevFeeds.length || plaidBankFound, newFeed: currentFeeds.find((feed) => !prevFeeds.includes(feed)) as CompanyCardFeedWithDomainID | undefined, }; } diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 5fc6a08e12f5..324fb3d80da7 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -60,14 +60,13 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const plaidToken = addNewCard?.data?.publicToken ?? assignCard?.cardToAssign?.plaidAccessToken; const isPlaid = !!plaidToken; const url = getCompanyCardBankConnection(policyID, bankName); - const [cardFeeds, , rawCardFeeds] = useCardFeeds(policyID); + const [cardFeeds] = useCardFeeds(policyID); const [isConnectionCompleted, setConnectionCompleted] = useState(false); const prevFeedsData = usePrevious(cardFeeds); const isFeedExpired = feed ? isSelectedFeedExpired(cardFeeds?.[feed]) : false; - const rawFeedKeys = useMemo(() => Object.keys(rawCardFeeds?.settings?.companyCards ?? {}), [rawCardFeeds?.settings?.companyCards]); const {isNewFeedConnected, newFeed} = useMemo( - () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCard?.data?.plaidConnectedFeed, rawFeedKeys), - [addNewCard?.data?.plaidConnectedFeed, cardFeeds, prevFeedsData, rawFeedKeys], + () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCard?.data?.plaidConnectedFeed), + [addNewCard?.data?.plaidConnectedFeed, cardFeeds, prevFeedsData], ); const headerTitleAddCards = !backTo ? translate('workspace.companyCards.addCards') : undefined; const headerTitle = feed ? translate('workspace.companyCards.assignCard') : headerTitleAddCards; @@ -75,7 +74,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {updateBrokenConnection, isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); - const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid; const activityReasonAttributes: SkeletonSpanReasonAttributes = { context: 'BankConnection', @@ -141,13 +139,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC // Handle add new card flow if (isNewFeedConnected) { - if (isDuplicateFeed) { - setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: true}}); - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - return; - } - if (newFeed) { updateSelectedFeed(newFeed, policyID); } @@ -161,7 +152,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC } }, [ isNewFeedConnected, - isDuplicateFeed, newFeed, policyID, url, diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 662a585cca85..a0f6c335d18b 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import ActivityIndicator from '@components/ActivityIndicator'; import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; @@ -56,16 +56,15 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD); const {feed: bankNameFromRoute, backTo, policyID: policyIDFromRoute} = route?.params ?? {}; const policyID = policyIDFromProps ?? policyIDFromRoute; - const [cardFeeds, , rawCardFeeds] = useCardFeeds(policyID); + const [cardFeeds] = useCardFeeds(policyID); const prevFeedsData = usePrevious(cardFeeds); const illustrations = useMemoizedLazyIllustrations(['PendingBank']); - const shouldBlockWindowOpenRef = useRef(false); + const [shouldBlockWindowOpen, setShouldBlockWindowOpen] = useState(false); const selectedBank = addNewCard?.data?.selectedBank; const bankName = feed ? getBankName(getCompanyCardFeed(feed)) : (bankNameFromRoute ?? addNewCard?.data?.plaidConnectedFeed ?? selectedBank); - const rawFeedKeys = useMemo(() => Object.keys(rawCardFeeds?.settings?.companyCards ?? {}), [rawCardFeeds?.settings?.companyCards]); const {isNewFeedConnected, newFeed} = useMemo( - () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCard?.data?.plaidConnectedFeed, rawFeedKeys), - [addNewCard?.data?.plaidConnectedFeed, cardFeeds, prevFeedsData, rawFeedKeys], + () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCard?.data?.plaidConnectedFeed), + [addNewCard?.data?.plaidConnectedFeed, cardFeeds, prevFeedsData], ); const {isOffline} = useNetwork(); const plaidToken = addNewCard?.data?.publicToken ?? assignCard?.cardToAssign?.plaidAccessToken; @@ -79,7 +78,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const onImportPlaidAccounts = useImportPlaidAccounts(policyID); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); - const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid; const onOpenBankConnectionFlow = useCallback(() => { if (!url) { @@ -152,16 +150,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC // Handle add new card flow if (isNewFeedConnected) { - shouldBlockWindowOpenRef.current = true; + setShouldBlockWindowOpen(true); customWindow?.close(); - - if (isDuplicateFeed) { - setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: true}}); - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - return; - } - if (newFeed) { updateSelectedFeed(newFeed, policyID); } @@ -170,7 +160,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); return; } - if (!shouldBlockWindowOpenRef.current) { + if (!shouldBlockWindowOpen) { if (isPlaid) { onImportPlaidAccounts(); return; @@ -182,8 +172,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC }, [ isNewFeedConnected, isAllFeedsResultLoading, + shouldBlockWindowOpen, isBlockedToAddNewFeeds, - isDuplicateFeed, newFeed, policyID, url, diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index e0cc0d567942..6eec067f5f91 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -1,5 +1,4 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; -import ConfirmModal from '@components/ConfirmModal'; import DecisionModal from '@components/DecisionModal'; import WorkspaceCompanyCardsTable from '@components/Tables/WorkspaceCompanyCardsTable'; import useAssignCard from '@hooks/useAssignCard'; @@ -7,7 +6,6 @@ import useCompanyCards from '@hooks/useCompanyCards'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; @@ -17,9 +15,8 @@ import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import {getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; -import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage, setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards'; +import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; @@ -90,12 +87,6 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) { const [shouldShowOfflineModal, setShouldShowOfflineModal] = useState(false); const {assignCard, isAssigningCardDisabled} = useAssignCard({feedName, policyID, setShouldShowOfflineModal}); - const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD); - const isDuplicateFeedDetected = !!addNewCard?.data?.isDuplicateFeedDetected; - - const closeDuplicateFeedModal = useCallback(() => { - setAddNewCompanyCardStepAndData({data: {isDuplicateFeedDetected: false}}); - }, []); return ( setShouldShowOfflineModal(false)} /> - - ); } diff --git a/src/types/onyx/CardFeeds.ts b/src/types/onyx/CardFeeds.ts index 5d2b0ba1853b..05592ba30b6c 100644 --- a/src/types/onyx/CardFeeds.ts +++ b/src/types/onyx/CardFeeds.ts @@ -306,9 +306,6 @@ type AddNewCardFeedData = { /** Plaid accounts */ plaidAccounts?: LinkAccount[] | PlaidAccount[]; - - /** Whether a duplicate Plaid feed was detected during bank connection */ - isDuplicateFeedDetected?: boolean; }; /** Issue new card flow steps */ From 532ec6508c8327162d2119700313eecfbc205c41 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Wed, 20 May 2026 15:38:25 +0000 Subject: [PATCH 17/31] Show duplicate Plaid feed modal directly in BankConnection Instead of using Onyx state to communicate between BankConnection and WorkspaceCompanyCardsPage (which had timing issues), show the ConfirmModal directly inside BankConnection using local component state. When a duplicate feed is detected (isNewFeedConnected && !newFeed && isPlaid), the modal appears immediately. On dismiss, navigation proceeds to the company cards page. Co-authored-by: Joel Davies --- src/languages/en.ts | 4 +++ src/languages/es.ts | 4 +++ .../BankConnection/index.native.tsx | 26 ++++++++++++++++++ .../companyCards/BankConnection/index.tsx | 27 +++++++++++++++++++ 4 files changed, 61 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 162917a09e3a..ce50e696244c 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5346,6 +5346,10 @@ const translations = { confirmText: 'Report issue', cancelText: 'Skip', }, + duplicateFeedModal: { + title: 'Card feed already connected', + prompt: "You can't add the same card feed to the same workspace twice.", + }, csvColumns: { cardNumber: 'Card number', postedDate: 'Date', diff --git a/src/languages/es.ts b/src/languages/es.ts index bd7ecc2ce80d..d5450409dce4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5157,6 +5157,10 @@ ${amount} para ${merchant} - ${date}`, confirmText: 'Informar problema', cancelText: 'Saltar', }, + duplicateFeedModal: { + title: 'Feed de tarjeta ya conectado', + prompt: 'No puedes añadir el mismo feed de tarjeta al mismo espacio de trabajo dos veces.', + }, csvColumns: { cardNumber: 'Número de la tarjeta', postedDate: 'Fecha', diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 324fb3d80da7..87f13113ba47 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -4,6 +4,7 @@ import type {WebViewNavigation} from 'react-native-webview'; import {WebView} from 'react-native-webview'; import ActivityIndicator from '@components/ActivityIndicator'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; +import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useCardFeeds from '@hooks/useCardFeeds'; @@ -74,6 +75,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {updateBrokenConnection, isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); + const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid; + const [isDuplicateModalVisible, setIsDuplicateModalVisible] = useState(false); const activityReasonAttributes: SkeletonSpanReasonAttributes = { context: 'BankConnection', @@ -139,6 +142,11 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC // Handle add new card flow if (isNewFeedConnected) { + if (isDuplicateFeed) { + setIsDuplicateModalVisible(true); + return; + } + if (newFeed) { updateSelectedFeed(newFeed, policyID); } @@ -152,6 +160,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC } }, [ isNewFeedConnected, + isDuplicateFeed, newFeed, policyID, url, @@ -214,6 +223,23 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC /> )} + { + setIsDuplicateModalVisible(false); + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + }} + onCancel={() => { + setIsDuplicateModalVisible(false); + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + }} + /> ); } diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index a0f6c335d18b..4d99a92c56ce 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -2,6 +2,7 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import ActivityIndicator from '@components/ActivityIndicator'; import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; +import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; @@ -78,6 +79,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const onImportPlaidAccounts = useImportPlaidAccounts(policyID); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); + const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid; + const [isDuplicateModalVisible, setIsDuplicateModalVisible] = useState(false); const onOpenBankConnectionFlow = useCallback(() => { if (!url) { @@ -152,6 +155,12 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC if (isNewFeedConnected) { setShouldBlockWindowOpen(true); customWindow?.close(); + + if (isDuplicateFeed) { + setIsDuplicateModalVisible(true); + return; + } + if (newFeed) { updateSelectedFeed(newFeed, policyID); } @@ -174,6 +183,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isAllFeedsResultLoading, shouldBlockWindowOpen, isBlockedToAddNewFeeds, + isDuplicateFeed, newFeed, policyID, url, @@ -235,6 +245,23 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC onBackButtonPress={handleBackButtonPress} /> {getContent()} + { + setIsDuplicateModalVisible(false); + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + }} + onCancel={() => { + setIsDuplicateModalVisible(false); + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + }} + /> ); } From 9756c43f3afe49ce561af46608e6d6a69e171f25 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Thu, 21 May 2026 07:49:51 +0000 Subject: [PATCH 18/31] Revert unrelated language file changes, keep only duplicateFeedModal in en.ts Co-authored-by: Joel Davies --- src/languages/de.ts | 140 ++++++++++++++++++++++------------- src/languages/en.ts | 4 +- src/languages/es.ts | 154 +++++++++++++++++++++++---------------- src/languages/fr.ts | 147 ++++++++++++++++++++++++------------- src/languages/it.ts | 141 ++++++++++++++++++++++------------- src/languages/ja.ts | 138 ++++++++++++++++++++++------------- src/languages/nl.ts | 139 ++++++++++++++++++++++------------- src/languages/pl.ts | 140 ++++++++++++++++++++++------------- src/languages/pt-BR.ts | 139 ++++++++++++++++++++++------------- src/languages/zh-hans.ts | 131 ++++++++++++++++++++------------- 10 files changed, 817 insertions(+), 456 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index b22f6407f8a1..c77a6ba1bd60 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -261,6 +261,7 @@ const translations: TranslationDeepObject = { reset: 'Zurücksetzen', done: 'Fertig', more: 'Mehr', + other: 'Andere', debitCard: 'Debitkarte', bankAccount: 'Bankkonto', personalBankAccount: 'Persönliches Bankkonto', @@ -957,8 +958,6 @@ const translations: TranslationDeepObject = { personalSubtitle: 'Geldbörse', }, }, - assignedCards: 'Ihre Expensify Karten', - assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} verbleibend`, announcements: 'Ankündigungen', discoverSection: { title: 'Entdecken', @@ -1038,6 +1037,12 @@ const translations: TranslationDeepObject = { other: (pluralCount: number) => `Verbleibende Zeit: ${pluralCount} Tage`, }), }, + yourSpend: { + title: 'Ihre Ausgaben', + awaitingApproval: 'Ausstehende Genehmigung', + repaidLast30Days: 'In den letzten 30 Tagen zurückgezahlt', + recentTransactions: ({lastFour}: {lastFour: string}) => `Aktuelle Transaktionen • ${lastFour}`, + }, }, allSettingsScreen: { subscription: 'Abonnement', @@ -2540,6 +2545,11 @@ ${amount} für ${merchant} – ${date}`, frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Diese Karte wurde von ${person} gesperrt. Bitte kontaktiere einen Admin, um sie zu entsperren.`, spendRules: 'Ausgaberichtlinien', editSpendRules: 'Ausgaberegeln bearbeiten', + cardUnblocked: 'Karte entsperrt!', + cardUnblockedDescription: 'Möglicherweise werden Sie beim nächsten Einkauf aufgefordert, Ihre Karte in das Lesegerät einzustecken.', + pinBlocked: 'Ihre Karte wurde aufgrund falscher PIN-Eingaben gesperrt.', + unblock: 'Entsperren', + unblockCard: 'Karte entsperren', }, workflowsPage: { workflowTitle: 'Ausgaben', @@ -3010,6 +3020,8 @@ ${amount} für ${merchant} – ${date}`, title: 'Arbeits-E-Mail konnte nicht hinzugefügt werden', subtitle: (workEmail: string | undefined) => `Wir konnten ${workEmail} nicht hinzufügen. Bitte versuche es später in den Einstellungen erneut oder chatte mit Concierge, um Unterstützung zu erhalten.`, + workAccountClosedSubtitle: + 'Das Arbeitskonto, das mit dieser E-Mail-Adresse verknüpft ist, wurde geschlossen. Bitte wenden Sie sich an Ihre Unternehmensadministratorin bzw. Ihren Unternehmensadministrator, um es zu reaktivieren, oder registrieren Sie sich mit einer anderen E-Mail-Adresse.', }, tasks: { testDriveAdminTask: { @@ -6742,6 +6754,58 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `HR-Integrationen sind nur im Control-Tarif verfügbar, ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, }, + approvalSubmit: { + title: 'Genehmigungen', + description: 'Konfigurieren Sie zentral, an wen alle Mitglieder einreichen, indem Sie Genehmigungen aktivieren.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Genehmigungen sind in den Collect- und Control-Tarifen verfügbar, beginnend bei ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, + }, + companyCardSubmit: { + title: 'Firmenkarten', + description: `Bringe deine eigene Firmenkarte zu Expensify, um automatische Importe, automatische Kategorisierung, anpassbare Regeln und integrierte Abstimmung zu erhalten.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Der Import von Firmenkreditkarten ist in den Collect- und Control-Tarifen verfügbar, beginnend bei ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, + }, + travelSubmit: { + title: 'Expensify Travel', + description: 'Buche weltweit vergünstigte Flüge, Hotels, Autos und Züge direkt in Expensify – mit Duty-of-Care-Berichten und integrierter Spesenverwaltung.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Expensify Travel ist in den Collect- und Control-Tarifen verfügbar, beginnend bei ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, + }, + roles: { + title: 'Rollen', + description: 'Weise verschiedenen Mitgliedern unterschiedliche Rollen zu, um Sichtbarkeit und Kontrolle bei Bedarf zu erhöhen oder zu verringern.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Rollen sind in den Collect- und Control-Tarifen verfügbar, beginnend ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, + }, + payments: { + title: 'Zahlungen', + description: 'Erstatte Mitarbeitenden direkt von deinem Geschäftskonto.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Zahlungen sind in den Collect- und Control-Tarifen verfügbar, ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, + }, + accounting: { + title: 'Buchhaltung', + description: + 'Synchronisiere Kategorien, Tags, Steuersätze und mehr aus deinem Buchhaltungssystem mit Expensify und exportiere Spesenberichte sowie Kartentransaktionen – ganz ohne Tippen (oder Tippfehler)!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Buchhaltung ist in den Tarifen Collect und Control verfügbar, ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, + }, + expensifyCard: { + title: 'Expensify Card', + description: + 'Gib Firmenkarten (einschließlich virtueller Karten) direkt von deinem eigenen Bankkonto aus, um in Echtzeit die Ausgaben zu kontrollieren – mit einer untrennbaren Verbindung und bis zu 2 % Cashback!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Die Expensify Card ist in den Collect- und Control-Tarifen verfügbar, ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, + upgradeButton: 'Upgrade durchführen und aktivieren', + }, + invoicing: { + title: 'Rechnungsstellung', + description: + 'Erstelle, versende und verfolge professionelle Rechnungen – alles in Expensify. Erhalte dein Geld schneller mit integrierten Zahlungen und vollständiger Transparenz in Echtzeit.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Die Rechnungsstellung ist in den Collect- und Control-Tarifen verfügbar, beginnend ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, + }, }, downgrade: { commonFeatures: { @@ -7078,8 +7142,32 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc }, hr: { title: 'Personalwesen', + connections: 'Verbindungen', subtitle: 'HR-Tools verbinden und Mitarbeitergenehmigungen synchron halten.', - settingsTitle: 'Gusto-Einstellungen', + connect: 'Verbinden', + syncNow: 'Jetzt synchronisieren', + disconnect: 'Trennen', + disconnectTitle: (providerName: string) => `${providerName} trennen`, + disconnectPrompt: (providerName: string) => `Möchten Sie ${providerName} wirklich trennen?`, + lastSync: (relativeDate: string) => `Zuletzt synchronisiert ${relativeDate}`, + syncError: (providerName: string) => `Verbindung zu ${providerName} nicht möglich`, + connectionDescription: (providerName: string) => `Verbinden Sie ${providerName}, um Mitarbeitergenehmigungen mit Ihrem Workspace zu synchronisieren.`, + approvalMode: 'Genehmigungsmodus', + finalApprover: 'Endgültige:r Genehmiger:in', + notSet: 'Nicht festgelegt', + approvalModeDescription: (providerName: string) => `Mitglieder und Manager sind für die Synchronisation mit ${providerName} eingerichtet.`, + approvalModeWarningTitle: 'Genehmigungsmodus ändern?', + approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => + `Möchten Sie den Genehmigungsmodus für diesen Workspace wirklich ändern? Erfahren Sie mehr über die verschiedenen ${providerName}-aktivierten Workflow-Modi auf unserer Hilfeseite.`, + approvalModeWarningConfirm: 'Genehmigungsmodus ändern', + approvalModes: { + basic: {label: 'Einfache Genehmigung', description: 'Alle Benutzer reichen zur Bearbeitung und Genehmigung bei einer einzigen Person ein.'}, + manager: { + label: 'Manager-Genehmigung', + description: (providerName: string) => `Mitarbeitende reichen Berichte bei ihrer in ${providerName} hinterlegten direkten Führungskraft ein.`, + }, + custom: {label: 'Benutzerdefinierte Genehmigung', description: 'Ich richte Genehmigungs-Workflows in Expensify manuell ein.'}, + }, syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -7103,27 +7191,6 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc }, gusto: { title: 'Gusto', - approvalMode: 'Genehmigungsmodus', - finalApprover: 'Endgültige:r Genehmiger:in', - notSet: 'Nicht festgelegt', - approvalModeDescription: 'Mitglieder und Manager sind für die Synchronisation mit Gusto eingerichtet.', - approvalModeWarningTitle: 'Genehmigungsmodus ändern?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `Möchten Sie den Genehmigungsmodus für diesen Workspace wirklich ändern? Erfahren Sie mehr über die verschiedenen Gusto-aktivierten Workflow-Modi auf unserer Hilfeseite.`, - approvalModeWarningConfirm: 'Genehmigungsmodus ändern', - approvalModes: { - basic: {label: 'Einfache Genehmigung', description: 'Alle Benutzer reichen zur Bearbeitung und Genehmigung bei einer einzigen Person ein.'}, - manager: {label: 'Managergenehmigung', description: 'Mitarbeitende reichen Berichte bei ihrer in Gusto hinterlegten direkten Führungskraft ein.'}, - custom: {label: 'Benutzerdefinierte Genehmigung', description: 'Ich richte Genehmigungs-Workflows in Expensify manuell ein.'}, - }, - connect: 'Verbinden', - connectionDescription: 'Verbinde Gusto, um Mitarbeitergenehmigungen mit deinem Workspace zu synchronisieren.', - syncNow: 'Jetzt synchronisieren', - disconnect: 'Trennen', - lastSync: (relativeDate: string) => `Zuletzt synchronisiert ${relativeDate}`, - syncError: 'Verbindung zu Gusto nicht möglich', - disconnectTitle: 'Gusto trennen', - disconnectPrompt: 'Bist du sicher, dass du Gusto trennen möchtest?', syncResults: { title: 'Gusto-Synchronisierungsergebnisse', successTitle: 'Ihre Gusto-Verbindung wurde erfolgreich synchronisiert!', @@ -7136,33 +7203,8 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc }), }, }, - merge: { - approvalMode: 'Genehmigungsmodus', - finalApprover: 'Endgültige:r Genehmiger:in', - }, zenefits: { title: 'TriNet', - connect: 'Verbinden', - syncNow: 'Jetzt synchronisieren', - disconnect: 'Verbindung trennen', - lastSync: (relativeDate: string) => `Zuletzt synchronisiert ${relativeDate}`, - syncError: 'Verbindung zu TriNet nicht möglich', - disconnectTitle: 'TriNet trennen', - disconnectPrompt: 'Sind Sie sicher, dass Sie die Verbindung zu TriNet trennen möchten?', - connectionDescription: 'Verbinden Sie TriNet, um Mitarbeitergenehmigungen mit Ihrem Workspace zu synchronisieren.', - approvalMode: 'Genehmigungsmodus', - finalApprover: 'Letzte genehmigende Person', - notSet: 'Nicht festgelegt', - approvalModeDescription: 'Mitglieder und Manager sind für die Synchronisierung mit TriNet eingerichtet.', - approvalModeWarningTitle: 'Genehmigungsmodus ändern?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `Sind Sie sicher, dass Sie den Genehmigungsmodus für diesen Workspace ändern möchten? Erfahren Sie mehr über die verschiedenen TriNet-fähigen Workflow-Modi auf unserer Hilfeseite.`, - approvalModeWarningConfirm: 'Genehmigungsmodus ändern', - approvalModes: { - basic: {label: 'Einfache Genehmigung', description: 'Alle Benutzer reichen an eine einzige Person zur Bearbeitung und Genehmigung ein.'}, - manager: {label: 'Managergenehmigung', description: 'Mitarbeitende reichen Berichte bei ihrer in TriNet hinterlegten direkten Führungskraft ein.'}, - custom: {label: 'Benutzerdefinierte Genehmigung', description: 'Ich richte Genehmigungsabläufe in Expensify manuell ein.'}, - }, }, }, }, diff --git a/src/languages/en.ts b/src/languages/en.ts index ce50e696244c..c883abf3cd7e 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7173,8 +7173,8 @@ const translations = { description: 'For employees looking to submit expenses to their employer.', }, }, - description: "Choose a plan that's right for you.", - subscriptionLink: 'Learn more', + description: "Choose a plan that's right for you. For a detailed list of features and pricing, check out our", + subscriptionLink: 'plan types and pricing help page', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `You've committed to 1 active member on the Control plan until your annual subscription ends on ${annualSubscriptionEndDate}. You can switch to pay-per-use subscription and downgrade to the Collect plan starting ${annualSubscriptionEndDate} by disabling auto-renew in`, other: `You've committed to ${count} active members on the Control plan until your annual subscription ends on ${annualSubscriptionEndDate}. You can switch to pay-per-use subscription and downgrade to the Collect plan starting ${annualSubscriptionEndDate} by disabling auto-renew in`, diff --git a/src/languages/es.ts b/src/languages/es.ts index d5450409dce4..cfcfe238925c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -217,6 +217,7 @@ const translations: TranslationDeepObject = { reset: 'Restablecer', done: 'Listo', more: 'Más', + other: 'Otro', debitCard: 'Tarjeta de débito', bankAccount: 'Cuenta bancaria', personalBankAccount: 'Cuenta bancaria personal', @@ -931,8 +932,12 @@ const translations: TranslationDeepObject = { other: (pluralCount: number) => `Tiempo restante: ${pluralCount} días`, }), }, - assignedCards: 'Tus tarjetas Expensify', - assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} restantes`, + yourSpend: { + title: 'Tu gasto', + awaitingApproval: 'Pendiente de aprobación', + repaidLast30Days: 'Reembolsado en los últimos 30 días', + recentTransactions: ({lastFour}: {lastFour: string}) => `Transacciones recientes • ${lastFour}`, + }, announcements: 'Anuncios', discoverSection: { title: 'Descubrir', @@ -2395,6 +2400,11 @@ ${amount} para ${merchant} - ${date}`, pinChanged: '¡PIN cambiado!', pinChangedHeader: 'PIN cambiado', pinChangedDescription: 'Ya puedes usar tu nuevo PIN.', + cardUnblocked: '¡Tarjeta desbloqueada!', + cardUnblockedDescription: 'Es posible que se te pida insertar tu tarjeta en el lector la próxima vez que realices una compra.', + pinBlocked: 'Tu tarjeta fue bloqueada por introducir el PIN incorrectamente varias veces.', + unblock: 'Desbloquear', + unblockCard: 'Desbloquear tarjeta', changePinAtATM: 'Cambia tu PIN en cualquier cajero automático', changePinAtATMDescription: 'Esto es obligatorio en tu región. Contacta a Concierge si tienes alguna pregunta.', freezeCard: 'Congelar tarjeta', @@ -2880,6 +2890,8 @@ ${amount} para ${merchant} - ${date}`, mergeBlockScreen: { title: 'No se pudo añadir el correo electrónico de trabajo', subtitle: (workEmail) => `No pudimos añadir ${workEmail}. Por favor, inténtalo de nuevo más tarde en Configuración o chatea con Concierge para obtener ayuda.`, + workAccountClosedSubtitle: + 'La cuenta de trabajo asociada a este correo electrónico está cerrada. Ponte en contacto con el administrador de tu empresa para reactivarla o regístrate con un correo electrónico diferente.', }, tasks: { testDriveAdminTask: { @@ -5157,10 +5169,6 @@ ${amount} para ${merchant} - ${date}`, confirmText: 'Informar problema', cancelText: 'Saltar', }, - duplicateFeedModal: { - title: 'Feed de tarjeta ya conectado', - prompt: 'No puedes añadir el mismo feed de tarjeta al mismo espacio de trabajo dos veces.', - }, csvColumns: { cardNumber: 'Número de la tarjeta', postedDate: 'Fecha', @@ -6249,8 +6257,32 @@ ${amount} para ${merchant} - ${date}`, }, hr: { title: 'HR', + connections: 'Conexiones', subtitle: 'Conecta herramientas de HR y mantén sincronizadas las aprobaciones de empleados.', - settingsTitle: 'Configuración de Gusto', + connect: 'Conectar', + syncNow: 'Sincronizar ahora', + disconnect: 'Desconectar', + disconnectTitle: (providerName: string) => `Desconectar ${providerName}`, + disconnectPrompt: (providerName: string) => `¿Seguro que quieres desconectar ${providerName}?`, + lastSync: (relativeDate: string) => `Última sincronización ${relativeDate}`, + syncError: (providerName: string) => `No se puede conectar con ${providerName}`, + connectionDescription: (providerName: string) => `Conecta ${providerName} para mantener sincronizadas las aprobaciones de empleados con tu espacio de trabajo.`, + approvalMode: 'Modo de aprobación', + finalApprover: 'Aprobador final', + notSet: 'No configurado', + approvalModeDescription: (providerName: string) => `Los miembros y gerentes están configurados para sincronizarse con ${providerName}.`, + approvalModeWarningTitle: '¿Cambiar modo de aprobación?', + approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => + `¿Seguro que quieres cambiar el modo de aprobación de este espacio de trabajo? Obtén más información sobre los diferentes modos de flujo de trabajo con ${providerName} en nuestro sitio de ayuda.`, + approvalModeWarningConfirm: 'Cambiar modo de aprobación', + approvalModes: { + basic: {label: 'Aprobación básica', description: 'Todos los usuarios envían a una sola persona para su procesamiento y aprobación.'}, + manager: { + label: 'Aprobación del gerente', + description: (providerName: string) => `Los empleados envían informes a su gerente directo configurado en ${providerName}.`, + }, + custom: {label: 'Aprobación personalizada', description: 'Configuraré manualmente los flujos de aprobación en Expensify.'}, + }, syncStageName: ({stage}) => { switch (stage) { case 'gustoSyncTitle': @@ -6265,6 +6297,8 @@ ${amount} para ${merchant} - ${date}`, return 'Cargando datos desde TriNet'; case 'zenefitsSyncProvisioning': return 'Aprovisionar empleados en la política'; + case 'mergeHRSyncTitle': + return 'Sincronización de empleados de HR'; case 'jobDone': return 'Esperando a que se carguen los datos importados'; default: { @@ -6274,36 +6308,6 @@ ${amount} para ${merchant} - ${date}`, }, gusto: { title: 'Gusto', - connect: 'Conectar', - syncNow: 'Sincronizar ahora', - disconnect: 'Desconectar', - lastSync: (relativeDate) => `Última sincronización ${relativeDate}`, - syncError: 'No se puede conectar con Gusto', - disconnectTitle: 'Desconectar Gusto', - disconnectPrompt: '¿Seguro que quieres desconectar Gusto?', - connectionDescription: 'Conecta Gusto para mantener sincronizadas las aprobaciones de empleados con tu espacio de trabajo.', - approvalMode: 'Modo de aprobación', - finalApprover: 'Aprobador final', - notSet: 'No configurado', - approvalModeDescription: 'Los miembros y gerentes están configurados para sincronizarse con Gusto.', - approvalModeWarningTitle: '¿Cambiar modo de aprobación?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `¿Seguro que quieres cambiar el modo de aprobación de este espacio de trabajo? Obtén más información sobre los diferentes modos de flujo de trabajo con Gusto en nuestro sitio de ayuda.`, - approvalModeWarningConfirm: 'Cambiar modo de aprobación', - approvalModes: { - basic: { - label: 'Aprobación básica', - description: 'Todos los usuarios envían a una sola persona para su procesamiento y aprobación.', - }, - manager: { - label: 'Aprobación del gerente', - description: 'Los empleados envían informes a su gerente directo configurado en Gusto.', - }, - custom: { - label: 'Aprobación personalizada', - description: 'Configuraré manualmente los flujos de aprobación en Expensify.', - }, - }, syncResults: { title: 'Resultados de la sincronización de Gusto', successTitle: '¡Se sincronizó correctamente tu conexión con Gusto!', @@ -6316,33 +6320,8 @@ ${amount} para ${merchant} - ${date}`, }), }, }, - merge: { - approvalMode: 'Modo de aprobación', - finalApprover: 'Aprobador final', - }, zenefits: { title: 'TriNet', - connect: 'Conectar', - syncNow: 'Sincronizar ahora', - disconnect: 'Desconectar', - lastSync: (relativeDate: string) => `Última sincronización ${relativeDate}`, - syncError: 'No se puede conectar con TriNet', - disconnectTitle: 'Desconectar TriNet', - disconnectPrompt: '¿Seguro que quieres desconectar TriNet?', - connectionDescription: 'Conecta TriNet para mantener las aprobaciones de empleados sincronizadas con tu espacio de trabajo.', - approvalMode: 'Modo de aprobación', - finalApprover: 'Aprobador final', - notSet: 'No establecido', - approvalModeDescription: 'Los miembros y gestores están configurados para sincronizarse con TriNet.', - approvalModeWarningTitle: '¿Cambiar el modo de aprobación?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `¿Seguro que quieres cambiar el modo de aprobación de este espacio de trabajo? Obtén más información sobre los distintos modos de flujo de trabajo habilitados por TriNet en nuestro sitio de ayuda.`, - approvalModeWarningConfirm: 'Cambiar el modo de aprobación', - approvalModes: { - basic: {label: 'Aprobación básica', description: 'Todos los usuarios envían a una sola persona para su procesamiento y aprobación.'}, - manager: {label: 'Aprobación del responsable', description: 'Los empleados envían informes a su responsable directo configurado en TriNet.'}, - custom: {label: 'Aprobación personalizada', description: 'Configuraré manualmente los flujos de aprobación en Expensify.'}, - }, }, }, export: { @@ -6566,6 +6545,12 @@ ${amount} para ${merchant} - ${date}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => `Las aprobaciones avanzadas sólo están disponibles en el plan Controlar, con precios desde ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, }, + approvalSubmit: { + title: 'Aprobaciones', + description: 'Configura de forma centralizada a quién envían todos los miembros sus gastos al activar las aprobaciones.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => + `Las aprobaciones están disponibles en los planes Recopilar y Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, + }, categories: { title: 'Categorías', description: 'Las categorías te permiten rastrear y organizar gastos. Usa nuestras categorías predeterminadas o añade las tuyas propias.', @@ -6596,6 +6581,12 @@ ${amount} para ${merchant} - ${date}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => `Esto solo está disponible en el plan Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, }, + companyCardSubmit: { + title: 'Tarjetas de empresa', + description: 'Trae tu propia tarjeta de empresa a Expensify para obtener importación automática, categorización automática, reglas personalizables y conciliación integrada.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => + `La importación de tarjetas de empresa está disponible en los planes Recopilar y Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, + }, rules: { title: 'Reglas', description: `Las reglas se ejecutan en segundo plano y mantienen tus gastos bajo control para que no tengas que preocuparte por los detalles pequeños.\n\nExige detalles de los gastos, como recibos y descripciones, establece límites y valores predeterminados, y automatiza las aprobaciones y los pagos, todo en un mismo lugar.`, @@ -6623,6 +6614,12 @@ ${amount} para ${merchant} - ${date}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => `Los viajes están disponibles en el plan Recopilar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, }, + travelSubmit: { + title: 'Expensify Travel', + description: 'Reserva vuelos, hoteles, coches y trenes con descuento en todo el mundo desde Expensify, con informes de duty of care y gestión de gastos integrada.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => + `Expensify Travel está disponible en los planes Recopilar y Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, + }, reports: { title: 'Informes', description: 'Los informes te permiten agrupar gastos para un seguimiento y organización más fáciles.', @@ -6655,6 +6652,39 @@ ${amount} para ${merchant} - ${date}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => `Los múltiples niveles de aprobación solo están disponibles en el plan Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, }, + roles: { + title: 'Roles', + description: 'Asigna diferentes roles a diferentes miembros para aumentar o disminuir la visibilidad y el control según corresponda.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => + `Los roles están disponibles en los planes Recopilar y Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, + }, + payments: { + title: 'Pagos', + description: 'Reembolsa a los empleados directamente desde la cuenta bancaria de tu empresa.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => + `Los pagos están disponibles en los planes Recopilar y Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, + }, + accounting: { + title: 'Contabilidad', + description: + 'Sincroniza categorías, etiquetas, tasas de impuestos y más desde tu sistema contable a Expensify, además de exportar informes de gastos y transacciones de tarjetas, ¡sin necesidad de escribir (ni errores tipográficos)!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => + `La contabilidad está disponible en los planes Recopilar y Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, + }, + expensifyCard: { + title: 'Tarjeta Expensify', + description: + 'Emite tarjetas de empresa (incluidas tarjetas virtuales) directamente desde tu propia cuenta bancaria para obtener control de gastos en tiempo real con una conexión irrompible, ¡y hasta un 2% de reembolso!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => + `La Tarjeta Expensify está disponible en los planes Recopilar y Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, + upgradeButton: 'Actualizar y activar', + }, + invoicing: { + title: 'Facturación', + description: 'Crea, envía y rastrea facturas profesionales, todo dentro de Expensify. Recibe pagos más rápido con pagos integrados y visibilidad en tiempo real.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => + `La facturación está disponible en los planes Recopilar y Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, + }, note: (subscriptionLink) => `Mejore para acceder a esta función, o más información sobre nuestros planes y precios.`, pricing: { perActiveMember: 'por miembro activo al mes.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index afa06290d04e..f217296b269d 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -261,6 +261,7 @@ const translations: TranslationDeepObject = { reset: 'Réinitialiser', done: 'Terminé', more: 'Plus', + other: 'Autre', debitCard: 'Carte de débit', bankAccount: 'Compte bancaire', personalBankAccount: 'Compte bancaire personnel', @@ -960,8 +961,6 @@ const translations: TranslationDeepObject = { personalSubtitle: 'Portefeuille', }, }, - assignedCards: 'Vos cartes Expensify', - assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} restant`, announcements: 'Annonces', discoverSection: { title: 'Découvrir', @@ -1041,6 +1040,12 @@ const translations: TranslationDeepObject = { setupRules: 'Configurer les règles de dépense', inviteAccountant: 'Inviter votre comptable', }, + yourSpend: { + title: 'Vos dépenses', + awaitingApproval: 'En attente d’approbation', + repaidLast30Days: 'Remboursé au cours des 30 derniers jours', + recentTransactions: ({lastFour}: {lastFour: string}) => `Transactions récentes • ${lastFour}`, + }, }, allSettingsScreen: { subscription: 'Abonnement', @@ -2546,6 +2551,11 @@ ${amount} pour ${merchant} - ${date}`, frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Cette carte a été gelée par ${person}. Veuillez contacter un administrateur pour la dégeler.`, spendRules: 'Règles de dépense', editSpendRules: 'Modifier les règles de dépenses', + cardUnblocked: 'Carte débloquée !', + cardUnblockedDescription: 'Il se peut que vous soyez invité à insérer votre carte dans le lecteur la prochaine fois que vous effectuerez un achat.', + pinBlocked: 'Votre carte a été bloquée en raison de saisies de code PIN incorrectes.', + unblock: 'Débloquer', + unblockCard: 'Débloquer la carte', }, workflowsPage: { workflowTitle: 'Dépense', @@ -3016,6 +3026,8 @@ ${amount} pour ${merchant} - ${date}`, title: 'Impossible d’ajouter l’adresse e-mail professionnelle', subtitle: (workEmail: string | undefined) => `Nous n’avons pas pu ajouter ${workEmail}. Veuillez réessayer plus tard dans les Paramètres ou discuter avec Concierge pour obtenir de l’aide.`, + workAccountClosedSubtitle: + 'Le compte professionnel associé à cet e-mail est fermé. Veuillez contacter l’administrateur de votre entreprise pour le réactiver, ou inscrivez-vous avec un autre e-mail.', }, tasks: { testDriveAdminTask: { @@ -6764,6 +6776,59 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Les intégrations RH sont uniquement disponibles avec l’offre Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, }, + approvalSubmit: { + title: 'Approbations', + description: 'Configurez de manière centralisée à qui tous les membres soumettent en activant les approbations.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Les validations sont disponibles avec les forfaits Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, + }, + companyCardSubmit: { + title: 'Cartes d’entreprise', + description: `Apportez votre propre carte professionnelle dans Expensify pour bénéficier de l’importation automatique, de la catégorisation automatique, de règles personnalisables et du rapprochement intégré.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `L’importation de cartes d’entreprise est disponible avec les offres Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, + }, + travelSubmit: { + title: 'Expensify Travel', + description: + 'Réservez des vols, hôtels, voitures et trains à tarif réduit partout dans le monde depuis Expensify, avec rapports de devoir de diligence et gestion intégrée des dépenses.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Expensify Travel est disponible avec les forfaits Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, + }, + roles: { + title: 'Rôles', + description: 'Attribuez différents rôles aux différents membres pour augmenter ou réduire la visibilité et le contrôle selon les besoins.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Les rôles sont disponibles avec les formules Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, + }, + payments: { + title: 'Paiements', + description: 'Remboursez vos employé·e·s directement depuis le compte bancaire de votre entreprise.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Les paiements sont disponibles avec les offres Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, + }, + accounting: { + title: 'Comptabilité', + description: + 'Synchronisez les catégories, les tags, les taux de taxe et plus encore depuis votre système comptable vers Expensify, et exportez également les notes de frais et les transactions par carte – sans saisie (ni fautes de frappe) !', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `La comptabilité est disponible avec les offres Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, + }, + expensifyCard: { + title: 'Carte Expensify', + description: + 'Émettez des cartes d’entreprise (y compris des cartes virtuelles) directement depuis votre propre compte bancaire pour bénéficier d’un contrôle des dépenses en temps réel avec une connexion infaillible, et jusqu’à 2 % de cashback !', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `La carte Expensify est disponible avec les offres Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, + upgradeButton: 'Mettre à niveau et activer', + }, + invoicing: { + title: 'Facturation', + description: + 'Créez, envoyez et suivez des factures professionnelles, le tout dans Expensify. Soyez payé plus rapidement grâce aux paiements intégrés et à la visibilité en temps réel.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `La facturation est disponible avec les offres Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, + }, }, downgrade: { commonFeatures: { @@ -7100,8 +7165,38 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e }, hr: { title: 'RH', + connections: 'Connexions', subtitle: 'Connectez vos outils RH et gardez les approbations des employés synchronisées.', - settingsTitle: 'Paramètres Gusto', + connect: 'Connecter', + syncNow: 'Synchroniser maintenant', + disconnect: 'Déconnecter', + disconnectTitle: (providerName: string) => `Déconnecter ${providerName}`, + disconnectPrompt: (providerName: string) => `Êtes-vous sûr de vouloir déconnecter ${providerName} ?`, + lastSync: (relativeDate: string) => `Dernière synchronisation ${relativeDate}`, + syncError: (providerName: string) => `Impossible de se connecter à ${providerName}`, + connectionDescription: (providerName: string) => `Connectez ${providerName} pour synchroniser les approbations des employés avec votre espace de travail.`, + approvalMode: "Mode d'approbation", + finalApprover: 'Approbateur final', + notSet: 'Non défini', + approvalModeDescription: (providerName: string) => `Les membres et les responsables sont configurés pour se synchroniser avec ${providerName}.`, + approvalModeWarningTitle: 'Changer le mode d’approbation ?', + approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => + `Êtes-vous sûr·e de vouloir modifier le mode d’approbation de cet espace de travail ? En savoir plus sur les différents modes de workflow compatibles avec ${providerName} sur notre site d’aide.`, + approvalModeWarningConfirm: 'Modifier le mode d’approbation', + approvalModes: { + basic: { + label: 'Approbation simple', + description: 'Tous les utilisateurs soumettent à une seule personne pour traitement et approbation.', + }, + manager: { + label: 'Approbation du responsable', + description: (providerName: string) => `Les employé·e·s soumettent leurs rapports à leur responsable direct configuré dans ${providerName}.`, + }, + custom: { + label: 'Approbation personnalisée', + description: 'Je configurerai manuellement les circuits de validation dans Expensify.', + }, + }, syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -7125,27 +7220,6 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e }, gusto: { title: 'Gusto', - approvalMode: 'Mode d’approbation', - finalApprover: 'Approbateur final', - notSet: 'Non défini', - approvalModeDescription: 'Les membres et les responsables sont configurés pour se synchroniser avec Gusto.', - approvalModeWarningTitle: 'Changer le mode d’approbation ?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `Êtes-vous sûr·e de vouloir modifier le mode d’approbation de cet espace de travail ? En savoir plus sur les différents modes de workflow compatibles avec Gusto sur notre site d’aide.`, - approvalModeWarningConfirm: 'Modifier le mode d’approbation', - approvalModes: { - basic: {label: 'Approbation simple', description: 'Tous les utilisateurs soumettent à une seule personne pour traitement et approbation.'}, - manager: {label: 'Approbation du responsable', description: 'Les employé·e·s soumettent leurs rapports à leur responsable direct configuré dans Gusto.'}, - custom: {label: 'Approbation personnalisée', description: 'Je configurerai manuellement les circuits de validation dans Expensify.'}, - }, - connect: 'Connect', - connectionDescription: 'Connectez Gusto pour synchroniser les validations des employé·e·s avec votre espace de travail.', - syncNow: 'Synchroniser maintenant', - disconnect: 'Déconnecter', - lastSync: (relativeDate: string) => `Dernière synchronisation ${relativeDate}`, - syncError: 'Impossible de se connecter à Gusto', - disconnectTitle: 'Déconnecter Gusto', - disconnectPrompt: 'Voulez-vous vraiment déconnecter Gusto ?', syncResults: { title: 'Résultats de la synchronisation Gusto', successTitle: 'Connexion Gusto synchronisée avec succès !', @@ -7158,33 +7232,8 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e }), }, }, - merge: { - approvalMode: 'Mode d’approbation', - finalApprover: 'Approbateur final', - }, zenefits: { title: 'TriNet', - connect: 'Connecter', - syncNow: 'Synchroniser maintenant', - disconnect: 'Déconnecter', - lastSync: (relativeDate: string) => `Dernière synchronisation ${relativeDate}`, - syncError: 'Impossible de se connecter à TriNet', - disconnectTitle: 'Déconnecter TriNet', - disconnectPrompt: 'Voulez-vous vraiment déconnecter TriNet ?', - connectionDescription: 'Connectez TriNet pour synchroniser les approbations des employés avec votre espace de travail.', - approvalMode: 'Mode d’approbation', - finalApprover: 'Approbateur final', - notSet: 'Non défini', - approvalModeDescription: 'Les membres et les responsables sont configurés pour se synchroniser avec TriNet.', - approvalModeWarningTitle: 'Changer le mode d’approbation ?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `Voulez-vous vraiment modifier le mode d’approbation de cet espace de travail ? En savoir plus sur les différents modes de workflow compatibles TriNet sur notre site d’aide.`, - approvalModeWarningConfirm: 'Modifier le mode d’approbation', - approvalModes: { - basic: {label: 'Approbation de base', description: 'Tous les utilisateurs soumettent à une seule personne pour traitement et approbation.'}, - manager: {label: 'Approbation du responsable', description: 'Les employés soumettent des notes de frais à leur supérieur hiérarchique direct configuré dans TriNet.'}, - custom: {label: 'Approbation personnalisée', description: 'Je configurerai manuellement les workflows d’approbation dans Expensify.'}, - }, }, }, }, diff --git a/src/languages/it.ts b/src/languages/it.ts index 0c1d9780c449..04714349e740 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -261,6 +261,7 @@ const translations: TranslationDeepObject = { reset: 'Reimposta', done: 'Completato', more: 'Altro', + other: 'Altro', debitCard: 'Carta di debito', bankAccount: 'Conto bancario', personalBankAccount: 'Conto bancario personale', @@ -958,8 +959,6 @@ const translations: TranslationDeepObject = { personalSubtitle: 'Portafoglio', }, }, - assignedCards: 'Le tue Carte Expensify', - assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} rimanenti`, announcements: 'Annunci', discoverSection: { title: 'Scopri', @@ -1039,6 +1038,12 @@ const translations: TranslationDeepObject = { setupRules: 'Configura le regole di spesa', inviteAccountant: 'Invita il tuo commercialista', }, + yourSpend: { + title: 'Le tue spese', + awaitingApproval: 'In attesa di approvazione', + repaidLast30Days: 'Rimborsato negli ultimi 30 giorni', + recentTransactions: ({lastFour}: {lastFour: string}) => `Transazioni recenti • ${lastFour}`, + }, }, allSettingsScreen: { subscription: 'Abbonamento', @@ -2536,6 +2541,11 @@ ${amount} per ${merchant} - ${date}`, frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Questa carta è stata bloccata da ${person}. Contatta un amministratore per sbloccarla.`, spendRules: 'Regole di spesa', editSpendRules: 'Modifica regole di spesa', + cardUnblocked: 'Carta sbloccata!', + cardUnblockedDescription: 'Potrebbe esserti richiesto di inserire la carta nel lettore la prossima volta che effettui un acquisto.', + pinBlocked: 'La tua carta è stata bloccata a causa di PIN inseriti in modo errato.', + unblock: 'Sblocca', + unblockCard: 'Sblocca carta', }, workflowsPage: { workflowTitle: 'Spesa', @@ -3004,6 +3014,8 @@ ${amount} per ${merchant} - ${date}`, title: "Impossibile aggiungere l'email di lavoro", subtitle: (workEmail: string | undefined) => `Non è stato possibile aggiungere ${workEmail}. Riprova più tardi in Impostazioni oppure chatta con Concierge per ricevere assistenza.`, + workAccountClosedSubtitle: + 'L’account di lavoro associato a questa email è chiuso. Contatta l’amministratore della tua azienda per riattivarlo oppure registrati con un’altra email.', }, tasks: { testDriveAdminTask: { @@ -6727,6 +6739,59 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Le integrazioni HR sono disponibili solo con il piano Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, }, + approvalSubmit: { + title: 'Approvazioni', + description: 'Configura centralmente a chi tutti i membri inviano le note spese abilitando le approvazioni.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Le approvazioni sono disponibili nei piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, + }, + companyCardSubmit: { + title: 'Carte aziendali', + description: `Porta la tua carta aziendale in Expensify per ottenere importazione automatica, categorizzazione automatica, supporto per regole personalizzabili e riconciliazione integrata.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `L'importazione della carta aziendale è disponibile con i piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, + }, + travelSubmit: { + title: 'Expensify Travel', + description: + 'Prenota in tutto il mondo voli, hotel, auto e treni scontati direttamente da Expensify, con reportistica per il duty of care e gestione integrata delle note spese.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Expensify Travel è disponibile con i piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, + }, + roles: { + title: 'Ruoli', + description: 'Assegna ruoli diversi ai vari membri per aumentare o ridurre visibilità e controllo secondo le esigenze.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `I ruoli sono disponibili nei piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, + }, + payments: { + title: 'Pagamenti', + description: 'Rimborsa i dipendenti direttamente dal conto bancario aziendale.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `I pagamenti sono disponibili nei piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, + }, + accounting: { + title: 'Contabilità', + description: + 'Sincronizza categorie, tag, aliquote fiscali e altro dal tuo sistema contabile a Expensify ed esporta anche note spese e transazioni con carta, senza bisogno di digitare nulla (né di rischiare errori di battitura)!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `La contabilità è disponibile con i piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, + }, + expensifyCard: { + title: 'Carta Expensify', + description: + 'Emetti carte aziendali (incluse carte virtuali) direttamente dal tuo conto bancario per ottenere il controllo in tempo reale delle spese con una connessione infallibile e fino al 2% di cashback!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `La Expensify Card è disponibile con i piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, + upgradeButton: 'Aggiorna e abilita', + }, + invoicing: { + title: 'Fatturazione', + description: + 'Crea, invia e monitora fatture professionali, tutto all’interno di Expensify. Ricevi i pagamenti più velocemente con pagamenti integrati e visibilità in tempo reale.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `La fatturazione è disponibile nei piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, + }, }, downgrade: { commonFeatures: { @@ -7064,8 +7129,32 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, }, hr: { title: 'Risorse umane', + connections: 'Connessioni', subtitle: 'Collega gli strumenti HR e mantieni sincronizzate le approvazioni dei dipendenti.', - settingsTitle: 'Impostazioni Gusto', + connect: 'Connetti', + syncNow: 'Sincronizza ora', + disconnect: 'Disconnetti', + disconnectTitle: (providerName: string) => `Disconnetti ${providerName}`, + disconnectPrompt: (providerName: string) => `Sei sicuro di voler disconnettere ${providerName}?`, + lastSync: (relativeDate: string) => `Ultima sincronizzazione ${relativeDate}`, + syncError: (providerName: string) => `Impossibile connettersi a ${providerName}`, + connectionDescription: (providerName: string) => `Collega ${providerName} per mantenere sincronizzate le approvazioni dei dipendenti con il tuo spazio di lavoro.`, + approvalMode: 'Modalità di approvazione', + finalApprover: 'Approvatore finale', + notSet: 'Non impostato', + approvalModeDescription: (providerName: string) => `I membri e i responsabili sono configurati per la sincronizzazione con ${providerName}.`, + approvalModeWarningTitle: 'Cambiare modalità di approvazione?', + approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => + `Sei sicuro di voler cambiare la modalità di approvazione per questo spazio di lavoro? Scopri di più sulle diverse modalità di flusso di lavoro abilitate per ${providerName} nel nostro sito di assistenza.`, + approvalModeWarningConfirm: 'Cambia modalità di approvazione', + approvalModes: { + basic: {label: 'Approvazione di base', description: 'Tutti gli utenti inviano a una singola persona per l’elaborazione e l’approvazione.'}, + manager: { + label: 'Approvazione del responsabile', + description: (providerName: string) => `I dipendenti inviano i report al proprio responsabile diretto configurato in ${providerName}.`, + }, + custom: {label: 'Approvazione personalizzata', description: 'Imposterò manualmente i flussi di approvazione in Expensify.'}, + }, syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -7089,27 +7178,6 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, }, gusto: { title: 'Gusto', - approvalMode: 'Modalità approvazione', - finalApprover: 'Approvazione finale', - notSet: 'Non impostato', - approvalModeDescription: 'I membri e i responsabili sono configurati per la sincronizzazione con Gusto.', - approvalModeWarningTitle: 'Cambiare modalità di approvazione?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `Sei sicuro di voler cambiare la modalità di approvazione per questo spazio di lavoro? Scopri di più sulle diverse modalità di flusso di lavoro abilitate per Gusto nel nostro sito di assistenza.`, - approvalModeWarningConfirm: 'Cambia modalità di approvazione', - approvalModes: { - basic: {label: 'Approvazione di base', description: 'Tutti gli utenti inviano a una singola persona per l’elaborazione e l’approvazione.'}, - manager: {label: 'Approvazione del responsabile', description: 'I dipendenti inviano i report al proprio responsabile diretto configurato in Gusto.'}, - custom: {label: 'Approvazione personalizzata', description: 'Imposterò manualmente i flussi di approvazione in Expensify.'}, - }, - connect: 'Collega', - connectionDescription: 'Collega Gusto per sincronizzare le approvazioni dei dipendenti con il tuo spazio di lavoro.', - syncNow: 'Sincronizza ora', - disconnect: 'Disconnetti', - lastSync: (relativeDate: string) => `Ultima sincronizzazione ${relativeDate}`, - syncError: 'Impossibile connettersi a Gusto', - disconnectTitle: 'Disconnetti Gusto', - disconnectPrompt: 'Sei sicuro di voler disconnettere Gusto?', syncResults: { title: 'Risultati sincronizzazione Gusto', successTitle: 'Connessione a Gusto sincronizzata con successo!', @@ -7122,33 +7190,8 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, }), }, }, - merge: { - approvalMode: 'Modalità approvazione', - finalApprover: 'Approvazione finale', - }, zenefits: { title: 'TriNet', - connect: 'Connetti', - syncNow: 'Sincronizza ora', - disconnect: 'Disconnetti', - lastSync: (relativeDate: string) => `Ultima sincronizzazione ${relativeDate}`, - syncError: 'Impossibile connettersi a TriNet', - disconnectTitle: 'Disconnetti TriNet', - disconnectPrompt: 'Sei sicuro di voler disconnettere TriNet?', - connectionDescription: 'Collega TriNet per mantenere sincronizzate le approvazioni dei dipendenti con il tuo spazio di lavoro.', - approvalMode: 'Modalità approvazione', - finalApprover: 'Responsabile finale approvazione', - notSet: 'Non impostato', - approvalModeDescription: 'Membri e responsabili sono configurati per sincronizzarsi con TriNet.', - approvalModeWarningTitle: 'Cambiare modalità di approvazione?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `Sei sicuro di voler cambiare la modalità di approvazione per questo workspace? Scopri di più sulle diverse modalità di workflow abilitate per TriNet nel nostro sito di assistenza.`, - approvalModeWarningConfirm: 'Modifica modalità di approvazione', - approvalModes: { - basic: {label: 'Approvazione di base', description: 'Tutti gli utenti inviano a una sola persona per l’elaborazione e l’approvazione.'}, - manager: {label: 'Approvazione del manager', description: 'I dipendenti inviano i report al loro responsabile diretto configurato in TriNet.'}, - custom: {label: 'Approvazione personalizzata', description: 'Imposterò manualmente i flussi di approvazione in Expensify.'}, - }, }, }, }, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 7f1c803423dc..c6ee83df7138 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -261,6 +261,7 @@ const translations: TranslationDeepObject = { reset: 'リセット', done: '完了', more: 'その他', + other: 'その他', debitCard: 'デビットカード', bankAccount: '銀行口座', personalBankAccount: '個人銀行口座', @@ -941,8 +942,6 @@ const translations: TranslationDeepObject = { personalSubtitle: 'ウォレット', }, }, - assignedCards: 'お客様の Expensify カード', - assignedCardsRemaining: ({amount}: {amount: string}) => `残額:${amount}`, announcements: 'お知らせ', discoverSection: { title: '発見', @@ -1022,6 +1021,12 @@ const translations: TranslationDeepObject = { setupRules: '支出ルールを設定', inviteAccountant: '会計士を招待', }, + yourSpend: { + title: 'あなたの支出', + awaitingApproval: '承認待ち', + repaidLast30Days: '過去30日間に返済済み', + recentTransactions: ({lastFour}: {lastFour: string}) => `最近の取引 • ${lastFour}`, + }, }, allSettingsScreen: { subscription: 'サブスクリプション', @@ -2511,6 +2516,11 @@ ${date} の ${merchant} への ${amount}`, frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `このカードは${person}によって一時停止されました。解除するには管理者に連絡してください。`, spendRules: '支出ルール', editSpendRules: '支出ルールを編集', + cardUnblocked: 'カードのブロックを解除しました!', + cardUnblockedDescription: '次回お買い物をされる際に、カードをリーダーに挿入するよう求められる場合があります。', + pinBlocked: '暗証番号の誤入力が続いたため、カードをブロックしました。', + unblock: 'ブロック解除', + unblockCard: 'カードのブロックを解除', }, workflowsPage: { workflowTitle: '支出', @@ -2977,6 +2987,8 @@ ${date} の ${merchant} への ${amount}`, title: '勤務用メールアドレスを追加できませんでした', subtitle: (workEmail: string | undefined) => `${workEmail} を追加できませんでした。後で「設定」からもう一度お試しいただくか、ガイダンスについて Concierge にチャットでお問い合わせください。`, + workAccountClosedSubtitle: + 'このメールアドレスに関連付けられている業務用アカウントは停止されています。再有効化するには会社の管理者にご連絡いただくか、別のメールアドレスでサインアップしてください。', }, tasks: { testDriveAdminTask: { @@ -6653,6 +6665,56 @@ ${reportName} onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `人事システム連携は、${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり、1か月ごと。`}からのControlプランでのみご利用いただけます`, }, + approvalSubmit: { + title: '承認', + description: '承認機能を有効にして、全メンバーの提出先を一括設定しましょう。', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `承認機能は、Collect プランおよび Control プランで利用できます。料金は ${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からです。`, + }, + companyCardSubmit: { + title: '会社カード', + description: `お使いの会社カードをExpensifyに連携して、自動取込、自動分類、カスタマイズ可能なルール設定、そして統合された照合機能を利用しましょう。`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `会社カードのインポートは、Collect プランと Control プランで利用できます。料金は ${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からです。`, + }, + travelSubmit: { + title: 'Expensify トラベル', + description: 'Expensify から世界中の割引航空券、ホテル、レンタカー、列車を予約し、デューティ・オブ・ケアレポートと経費管理を統合して利用できます。', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Expensify Travel は Collect プランと Control プランで利用でき、${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からご利用いただけます`, + }, + roles: { + title: 'ロール', + description: 'メンバーごとに異なるロールを割り当て、必要に応じて閲覧権限や管理権限を増減させましょう。', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `ロールは、Collect および Control プランで利用できます。${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からご利用いただけます`, + }, + payments: { + title: '支払い', + description: '従業員への精算を、事業用銀行口座から直接行います。', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `支払い機能は、Collect プランおよび Control プランで利用できます。料金は ${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からです`, + }, + accounting: { + title: '会計', + description: '会計システムからExpensifyへカテゴリ、タグ、税率などを同期し、経費レポートやカード取引をエクスポートできます。手入力も、入力ミスの心配も不要です!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `会計機能は Collect プランと Control プランでご利用いただけます。${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`}からご利用可能です`, + }, + expensifyCard: { + title: 'Expensify カード', + description: + '自社の銀行口座から(バーチャルカードを含む)コーポレートカードを直接発行し、途切れない連携によるリアルタイムな支出管理と、最大2%のキャッシュバックを手に入れましょう!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Expensify Card は Collect プランと Control プランでご利用いただけます。料金は ${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からです。`, + upgradeButton: 'アップグレードして有効化', + }, + invoicing: { + title: '請求書発行', + description: 'Expensify 内でプロ仕様の請求書を作成・送信・追跡。統合された支払い機能とリアルタイムの可視性で、より早く入金を受けましょう。', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `請求書発行機能は、Collect プランと Control プランでご利用いただけます。${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`}からご利用可能です。`, + }, }, downgrade: { commonFeatures: { @@ -6986,8 +7048,32 @@ ${reportName} }, hr: { title: '人事', + connections: '接続', subtitle: '人事ツールを連携して、従業員の承認を常に同期させます。', - settingsTitle: 'Gusto 設定', + connect: '接続', + syncNow: '今すぐ同期', + disconnect: '切断', + disconnectTitle: (providerName: string) => `${providerName}を切断`, + disconnectPrompt: (providerName: string) => `${providerName}を切断してもよろしいですか?`, + lastSync: (relativeDate: string) => `最終同期: ${relativeDate}`, + syncError: (providerName: string) => `${providerName}に接続できません`, + connectionDescription: (providerName: string) => `${providerName}を接続して、従業員の承認をワークスペースと同期させましょう。`, + approvalMode: '承認モード', + finalApprover: '最終承認者', + notSet: '未設定', + approvalModeDescription: (providerName: string) => `メンバーとマネージャーは ${providerName} と同期するように設定されています。`, + approvalModeWarningTitle: '承認モードを変更しますか?', + approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => + `このワークスペースの承認モードを変更してもよろしいですか?${providerName} 対応の各ワークフローモードについては、ヘルプサイトで詳しくご覧いただけます。`, + approvalModeWarningConfirm: '承認モードを変更', + approvalModes: { + basic: {label: '基本承認', description: 'すべてのユーザーは、処理と承認のために 1 人の担当者に提出します。'}, + manager: { + label: 'マネージャー承認', + description: (providerName: string) => `従業員は、${providerName} で設定された直属のマネージャーにレポートを提出します。`, + }, + custom: {label: 'カスタム承認', description: 'Expensify で承認ワークフローを手動で設定します。'}, + }, syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -7011,27 +7097,6 @@ ${reportName} }, gusto: { title: 'Gusto', - approvalMode: '承認モード', - finalApprover: '最終承認者', - notSet: '未設定', - approvalModeDescription: 'メンバーとマネージャーは Gusto と同期するように設定されています。', - approvalModeWarningTitle: '承認モードを変更しますか?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `このワークスペースの承認モードを変更してもよろしいですか?Gusto 対応の各ワークフローモードについては、ヘルプサイトで詳しくご覧いただけます。`, - approvalModeWarningConfirm: '承認モードを変更', - approvalModes: { - basic: {label: '基本承認', description: 'すべてのユーザーは、処理と承認のために 1 人の担当者に提出します。'}, - manager: {label: 'マネージャー承認', description: '従業員は、Gusto で設定された直属のマネージャーにレポートを提出します。'}, - custom: {label: 'カスタム承認', description: 'Expensify で承認ワークフローを手動で設定します。'}, - }, - connect: '接続', - connectionDescription: 'Gusto を接続して、従業員の承認をワークスペースと同期させましょう。', - syncNow: '今すぐ同期', - disconnect: '切断', - lastSync: (relativeDate: string) => `最終同期:${relativeDate}`, - syncError: 'Gusto に接続できません', - disconnectTitle: 'Gusto の接続を解除', - disconnectPrompt: 'Gusto との接続を本当に解除しますか?', syncResults: { title: 'Gusto 同期結果', successTitle: 'Gusto との連携が正常に同期されました!', @@ -7044,33 +7109,8 @@ ${reportName} }), }, }, - merge: { - approvalMode: '承認モード', - finalApprover: '最終承認者', - }, zenefits: { title: 'TriNet', - connect: '接続', - syncNow: '今すぐ同期', - disconnect: '切断する', - lastSync: (relativeDate: string) => `最終同期日時:${relativeDate}`, - syncError: 'TriNet に接続できません', - disconnectTitle: 'TriNet の接続を解除', - disconnectPrompt: 'TriNet との接続を解除してもよろしいですか?', - connectionDescription: 'TriNet を接続して、従業員の承認をワークスペースと同期させましょう。', - approvalMode: '承認モード', - finalApprover: '最終承認者', - notSet: '未設定', - approvalModeDescription: 'メンバーとマネージャーは TriNet と同期するように設定されています。', - approvalModeWarningTitle: '承認モードを変更しますか?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `このワークスペースの承認モードを本当に変更してもよろしいですか?TriNet に対応した各種ワークフローモードの詳細は、ヘルプサイトをご覧ください。`, - approvalModeWarningConfirm: '承認モードを変更', - approvalModes: { - basic: {label: '基本承認', description: 'すべてのユーザーが、処理と承認のために 1 人の担当者へ経費を提出します。'}, - manager: {label: 'マネージャー承認', description: '従業員は、TriNet で設定された直属のマネージャーにレポートを提出します。'}, - custom: {label: 'カスタム承認', description: 'Expensify で承認ワークフローを手動で設定します。'}, - }, }, }, }, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index d2166cd9b64b..891386fd47b8 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -261,6 +261,7 @@ const translations: TranslationDeepObject = { reset: 'Opnieuw instellen', done: 'Klaar', more: 'Meer', + other: 'Overig', debitCard: 'Debetkaart', bankAccount: 'Bankrekening', personalBankAccount: 'Persoonlijke bankrekening', @@ -956,8 +957,6 @@ const translations: TranslationDeepObject = { personalSubtitle: 'Portemonnee', }, }, - assignedCards: 'Je Expensify Kaarten', - assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} resterend`, announcements: 'Aankondigingen', discoverSection: { title: 'Ontdekken', @@ -1037,6 +1036,12 @@ const translations: TranslationDeepObject = { setupRules: 'Uitgavenregels instellen', inviteAccountant: 'Nodig je accountant uit', }, + yourSpend: { + title: 'Je uitgaven', + awaitingApproval: 'In afwachting van goedkeuring', + repaidLast30Days: 'Terugbetaald in de afgelopen 30 dagen', + recentTransactions: ({lastFour}: {lastFour: string}) => `Recente transacties • ${lastFour}`, + }, }, allSettingsScreen: { subscription: 'Abonnement', @@ -2533,6 +2538,11 @@ ${amount} voor ${merchant} - ${date}`, frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Deze kaart is bevroren door ${person}. Neem contact op met een beheerder om deze te deblokkeren.`, spendRules: 'Bestedingsregels', editSpendRules: 'Uitgavenregels bewerken', + cardUnblocked: 'Kaart gedeblokkeerd!', + cardUnblockedDescription: 'Mogelijk wordt je de volgende keer dat je een aankoop doet gevraagd om je kaart in de lezer te steken.', + pinBlocked: 'Je kaart is geblokkeerd vanwege onjuiste pincode-invoer.', + unblock: 'Deblokkeren', + unblockCard: 'Kaart deblokkeren', }, workflowsPage: { workflowTitle: 'Uitgaven', @@ -3000,6 +3010,8 @@ ${amount} voor ${merchant} - ${date}`, mergeBlockScreen: { title: 'Werkmail kon niet worden toegevoegd', subtitle: (workEmail: string | undefined) => `We konden ${workEmail} niet toevoegen. Probeer het later opnieuw in Instellingen of chat met Concierge voor hulp.`, + workAccountClosedSubtitle: + 'De zakelijke account die aan dit e-mailadres is gekoppeld, is gesloten. Neem contact op met de beheerder van je bedrijf om het opnieuw te activeren, of meld je aan met een ander e-mailadres.', }, tasks: { testDriveAdminTask: { @@ -6706,6 +6718,57 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `HR-integraties zijn alleen beschikbaar in het Control-abonnement, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actief lid per maand.`}`, }, + approvalSubmit: { + title: 'Goedkeuringen', + description: 'Configureer centraal naar wie alle leden indienen door goedkeuringen in te schakelen.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Goedkeuringen zijn beschikbaar in de Collect- en Control-abonnementen, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, + }, + companyCardSubmit: { + title: 'Bedrijfskaarten', + description: `Neem je eigen bedrijfskaart mee naar Expensify voor automatische import, automatische categorisatie, ondersteuning voor aangepaste regels en geïntegreerde afstemming.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Bedrijfskaartimport is beschikbaar in de Collect- en Control-abonnementen, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, + }, + travelSubmit: { + title: 'Expensify Travel', + description: 'Boek wereldwijd voordelige vluchten, hotels, auto’s en treinen vanuit Expensify, met duty-of-carerapportage en geïntegreerd onkostbeheer.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Expensify Travel is beschikbaar in de Collect- en Control-abonnementen, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, + }, + roles: { + title: 'Rollen', + description: 'Wijs verschillende rollen toe aan verschillende leden om de zichtbaarheid en controle naar behoefte te verhogen of te verlagen.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Rollen zijn beschikbaar in de Collect- en Control-abonnementen, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, + }, + payments: { + title: 'Betalingen', + description: 'Vergoed medewerkers rechtstreeks vanaf je zakelijke bankrekening.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Betalingen zijn beschikbaar in de abonnementen Collect en Control, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, + }, + accounting: { + title: 'Boekhouding', + description: + 'Synchroniseer categorieën, labels, belastingtarieven en meer van je boekhoudsysteem naar Expensify, en exporteer onkostendeclaraties en kaarttransacties – zonder typwerk (of tikfouten)!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Boekhouding is beschikbaar in de Collect- en Control-abonnementen, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, + }, + expensifyCard: { + title: 'Expensify Card', + description: + 'Geef bedrijfskaarten (inclusief virtuele kaarten) direct uit vanaf je eigen bankrekening voor realtime controle over uitgaven met een onbreekbare koppeling en tot 2% cashback!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `De Expensify Card is beschikbaar bij de Collect- en Control-abonnementen, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, + upgradeButton: 'Upgrade en inschakelen', + }, + invoicing: { + title: 'Facturatie', + description: 'Maak, verstuur en volg professionele facturen, allemaal in Expensify. Laat je sneller betalen met geïntegreerde betalingen en realtime inzicht.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Facturatie is beschikbaar in de Collect- en Control-abonnementen, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, + }, }, downgrade: { commonFeatures: { @@ -7041,8 +7104,32 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, }, hr: { title: 'HR', + connections: 'Verbindingen', subtitle: 'Koppel HR-tools en houd goedkeuringen van medewerkers gesynchroniseerd.', - settingsTitle: 'Gusto-instellingen', + connect: 'Verbinden', + syncNow: 'Nu synchroniseren', + disconnect: 'Ontkoppelen', + disconnectTitle: (providerName: string) => `${providerName} ontkoppelen`, + disconnectPrompt: (providerName: string) => `Weet je zeker dat je ${providerName} wilt ontkoppelen?`, + lastSync: (relativeDate: string) => `Laatst gesynchroniseerd ${relativeDate}`, + syncError: (providerName: string) => `Kan geen verbinding maken met ${providerName}`, + connectionDescription: (providerName: string) => `Verbind ${providerName} om goedkeuringen van werknemers gesynchroniseerd te houden met je werkruimte.`, + approvalMode: 'Goedkeuringsmodus', + finalApprover: 'Eindgoedkeurder', + notSet: 'Niet ingesteld', + approvalModeDescription: (providerName: string) => `Leden en managers zijn ingesteld om te synchroniseren met ${providerName}.`, + approvalModeWarningTitle: 'Goedkeuringsmodus wijzigen?', + approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => + `Weet je zeker dat je de goedkeuringsmodus voor deze werkruimte wilt wijzigen? Lees meer over de verschillende ${providerName}-ondersteunde workflowmodi op onze help-site.`, + approvalModeWarningConfirm: 'Goedkeuringsmodus wijzigen', + approvalModes: { + basic: {label: 'Basisgoedkeuring', description: 'Alle gebruikers dienen bij één persoon in voor verwerking en goedkeuring.'}, + manager: { + label: 'Managergoedkeuring', + description: (providerName: string) => `Werknemers dienen rapporten in bij hun direct leidinggevende die is ingesteld in ${providerName}.`, + }, + custom: {label: 'Aangepaste goedkeuring', description: 'Ik stel goedkeuringsworkflows handmatig in in Expensify.'}, + }, syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -7066,27 +7153,6 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, }, gusto: { title: 'Gusto', - approvalMode: 'Goedkeuringsmodus', - finalApprover: 'Laatste fiatteur', - notSet: 'Niet ingesteld', - approvalModeDescription: 'Leden en managers zijn ingesteld om te synchroniseren met Gusto.', - approvalModeWarningTitle: 'Goedkeuringsmodus wijzigen?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `Weet je zeker dat je de goedkeuringsmodus voor deze werkruimte wilt wijzigen? Lees meer over de verschillende Gusto-ondersteunde workflowmodi op onze help-site.`, - approvalModeWarningConfirm: 'Goedkeuringsmodus wijzigen', - approvalModes: { - basic: {label: 'Basisgoedkeuring', description: 'Alle gebruikers dienen bij één persoon in voor verwerking en goedkeuring.'}, - manager: {label: 'Goedkeuring manager', description: 'Werknemers dienen rapporten in bij hun direct leidinggevende die is ingesteld in Gusto.'}, - custom: {label: 'Aangepaste goedkeuring', description: 'Ik stel goedkeuringsworkflows handmatig in in Expensify.'}, - }, - connect: 'Verbinden', - connectionDescription: 'Verbind Gusto om goedkeuringen van werknemers gesynchroniseerd te houden met je workspace.', - syncNow: 'Nu synchroniseren', - disconnect: 'Verbreken', - lastSync: (relativeDate: string) => `Laatste synchronisatie ${relativeDate}`, - syncError: 'Kan geen verbinding maken met Gusto', - disconnectTitle: 'Verbinding met Gusto verbreken', - disconnectPrompt: 'Weet je zeker dat je de verbinding met Gusto wilt verbreken?', syncResults: { title: 'Gusto-synchronisatieresultaten', successTitle: 'Je Gusto-verbinding is succesvol gesynchroniseerd!', @@ -7099,33 +7165,8 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, }), }, }, - merge: { - approvalMode: 'Goedkeuringsmodus', - finalApprover: 'Laatste fiatteur', - }, zenefits: { title: 'TriNet', - connect: 'Verbinden', - syncNow: 'Nu synchroniseren', - disconnect: 'Verbinding verbreken', - lastSync: (relativeDate: string) => `Laatst gesynchroniseerd ${relativeDate}`, - syncError: 'Kan geen verbinding maken met TriNet', - disconnectTitle: 'Koppeling met TriNet verbreken', - disconnectPrompt: 'Weet je zeker dat je TriNet wilt loskoppelen?', - connectionDescription: 'Verbind TriNet om goedkeuringen van medewerkers synchroon te houden met je werkruimte.', - approvalMode: 'Goedkeuringsmodus', - finalApprover: 'Laatste fiatteur', - notSet: 'Niet ingesteld', - approvalModeDescription: 'Leden en managers zijn ingesteld om te synchroniseren met TriNet.', - approvalModeWarningTitle: 'Goedkeuringsmodus wijzigen?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `Weet je zeker dat je de goedkeuringsmodus voor deze workspace wilt wijzigen? Lees meer over de verschillende TriNet-ondersteunde workflowmodi op onze help-pagina.`, - approvalModeWarningConfirm: 'Goedkeuringsmodus wijzigen', - approvalModes: { - basic: {label: 'Eenvoudige goedkeuring', description: 'Alle gebruikers dienen in bij één persoon voor verwerking en goedkeuring.'}, - manager: {label: 'Goedkeuring door manager', description: 'Werknemers dienen rapporten in bij hun direct leidinggevende die is ingesteld in TriNet.'}, - custom: {label: 'Aangepaste goedkeuring', description: 'Ik stel handmatig goedkeuringsworkflows in Expensify in.'}, - }, }, }, }, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 99dbf05b915e..91459e089731 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -261,6 +261,7 @@ const translations: TranslationDeepObject = { reset: 'Reset', done: 'Gotowe', more: 'Więcej', + other: 'Inne', debitCard: 'Karta debetowa', bankAccount: 'Konto bankowe', personalBankAccount: 'Prywatne konto bankowe', @@ -958,8 +959,6 @@ const translations: TranslationDeepObject = { personalSubtitle: 'Portfel', }, }, - assignedCards: 'Twoje Karty Expensify', - assignedCardsRemaining: ({amount}: {amount: string}) => `Pozostało ${amount}`, announcements: 'Ogłoszenia', discoverSection: { title: 'Odkryj', @@ -1039,6 +1038,12 @@ const translations: TranslationDeepObject = { setupRules: 'Skonfiguruj zasady wydatków', inviteAccountant: 'Zaproś swojego księgowego', }, + yourSpend: { + title: 'Twoje wydatki', + awaitingApproval: 'Oczekuje na zatwierdzenie', + repaidLast30Days: 'Spłacono w ciągu ostatnich 30 dni', + recentTransactions: ({lastFour}: {lastFour: string}) => `Ostatnie transakcje • ${lastFour}`, + }, }, allSettingsScreen: { subscription: 'Subskrypcja', @@ -2529,6 +2534,11 @@ ${amount} dla ${merchant} - ${date}`, frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Ta karta została zamrożona przez ${person}. Skontaktuj się z administratorem, aby ją odmrozić.`, spendRules: 'Zasady wydatków', editSpendRules: 'Edytuj zasady wydatków', + cardUnblocked: 'Karta odblokowana!', + cardUnblockedDescription: 'Przy następnych zakupach możesz zostać poproszony(-a) o włożenie karty do czytnika.', + pinBlocked: 'Twoja karta została zablokowana z powodu błędnego wprowadzenia kodu PIN.', + unblock: 'Odblokuj', + unblockCard: 'Odblokuj kartę', }, workflowsPage: { workflowTitle: 'Wydatki', @@ -2993,6 +3003,8 @@ ${amount} dla ${merchant} - ${date}`, mergeBlockScreen: { title: 'Nie udało się dodać służbowego adresu e-mail', subtitle: (workEmail: string | undefined) => `Nie udało się dodać ${workEmail}. Spróbuj ponownie później w Ustawieniach lub porozmawiaj z Concierge, aby uzyskać pomoc.`, + workAccountClosedSubtitle: + 'Konto służbowe powiązane z tym adresem e‑mail jest zamknięte. Skontaktuj się z administratorem w swojej firmie, żeby je ponownie aktywować, albo zarejestruj się przy użyciu innego adresu e‑mail.', }, tasks: { testDriveAdminTask: { @@ -6699,6 +6711,58 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Integracje HR są dostępne tylko w planie Control, zaczynającym się od ${formattedPrice} ${hasTeam2025Pricing ? `za osobę miesięcznie.` : `za aktywnego członka miesięcznie.`}`, }, + approvalSubmit: { + title: 'Zatwierdzenia', + description: 'Skonfiguruj centralnie, do kogo wszyscy członkowie składają raporty, włączając zatwierdzanie.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Zatwierdzanie jest dostępne w planach Collect i Control, zaczynając od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`}`, + }, + companyCardSubmit: { + title: 'Firmowe karty', + description: `Dodaj swoją firmową kartę do Expensify, aby uzyskać automatyczny import, automatyczną kategoryzację, obsługę konfigurowalnych reguł i zintegrowane uzgadnianie.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Importowanie kart firmowych jest dostępne w planach Collect i Control, zaczynających się od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`}`, + }, + travelSubmit: { + title: 'Podróże Expensify', + description: 'Rezerwuj na całym świecie tańsze loty, hotele, samochody i pociągi bezpośrednio w Expensify, z raportami duty of care i zintegrowanym zarządzaniem wydatkami.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Expensify Travel jest dostępny w planach Collect i Control, zaczynając od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`}`, + }, + roles: { + title: 'Role', + description: 'Przypisz różne role różnym członkom, aby w razie potrzeby zwiększyć lub zmniejszyć poziom widoczności i kontroli.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Role są dostępne w planach Collect i Control, zaczynając od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`}`, + }, + payments: { + title: 'Płatności', + description: 'Zwracaj pracownikom wydatki bezpośrednio z firmowego konta bankowego.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Płatności są dostępne w planach Collect i Control, zaczynając od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`}`, + }, + accounting: { + title: 'Księgowość', + description: + 'Synchronizuj kategorie, tagi, stawki podatkowe i więcej z Twojego systemu księgowego do Expensify, a także eksportuj raporty wydatków i transakcje kartą – bez przepisywania (i bez literówek)!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Księgowość jest dostępna w planach Collect i Control, zaczynając od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`}`, + }, + expensifyCard: { + title: 'Karta Expensify', + description: + 'Wydawaj firmowe karty (w tym karty wirtualne) bezpośrednio z własnego konta bankowego, aby zyskać kontrolę nad wydatkami w czasie rzeczywistym dzięki nierozerwalnemu połączeniu i nawet do 2% zwrotu gotówki!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Karta Expensify jest dostępna w planach Collect i Control, zaczynając od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`}`, + upgradeButton: 'Ulepsz i włącz', + }, + invoicing: { + title: 'Fakturowanie', + description: + 'Twórz, wysyłaj i śledź profesjonalne faktury — wszystko w Expensify. Otrzymuj płatności szybciej dzięki zintegrowanym płatnościom i wglądowi w czasie rzeczywistym.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Fakturowanie jest dostępne w planach Collect i Control, zaczynając od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`}`, + }, }, downgrade: { commonFeatures: { @@ -7035,8 +7099,32 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, }, hr: { title: 'HR', + connections: 'Połączenia', subtitle: 'Połącz narzędzia HR i utrzymuj zgody pracowników w synchronizacji.', - settingsTitle: 'Ustawienia Gusto', + connect: 'Połącz', + syncNow: 'Synchronizuj teraz', + disconnect: 'Odłącz', + disconnectTitle: (providerName: string) => `Odłącz ${providerName}`, + disconnectPrompt: (providerName: string) => `Czy na pewno chcesz odłączyć ${providerName}?`, + lastSync: (relativeDate: string) => `Ostatnia synchronizacja ${relativeDate}`, + syncError: (providerName: string) => `Nie można połączyć z ${providerName}`, + connectionDescription: (providerName: string) => `Połącz ${providerName}, aby synchronizować akceptacje pracowników z Twoim miejscem pracy.`, + approvalMode: 'Tryb zatwierdzania', + finalApprover: 'Ostateczny zatwierdzający', + notSet: 'Nie ustawiono', + approvalModeDescription: (providerName: string) => `Członkowie i menedżerowie są skonfigurowani do synchronizacji z ${providerName}.`, + approvalModeWarningTitle: 'Zmienić tryb zatwierdzania?', + approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => + `Czy na pewno chcesz zmienić tryb zatwierdzania dla tego obszaru roboczego? Dowiedz się więcej o różnych trybach przepływu pracy obsługiwanych przez ${providerName} w naszej witrynie pomocy.`, + approvalModeWarningConfirm: 'Zmień tryb zatwierdzania', + approvalModes: { + basic: {label: 'Podstawowe zatwierdzanie', description: 'Wszyscy użytkownicy przesyłają wydatki do jednej osoby w celu ich przetworzenia i zatwierdzenia.'}, + manager: { + label: 'Zatwierdzanie przez menedżera', + description: (providerName: string) => `Pracownicy przesyłają raporty do swojego bezpośredniego menedżera skonfigurowanego w ${providerName}.`, + }, + custom: {label: 'Niestandardowe zatwierdzanie', description: 'Ręcznie skonfiguruję procesy zatwierdzania w Expensify.'}, + }, syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -7060,27 +7148,6 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, }, gusto: { title: 'Gusto', - approvalMode: 'Tryb zatwierdzania', - finalApprover: 'Ostateczny zatwierdzający', - notSet: 'Nie ustawiono', - approvalModeDescription: 'Członkowie i menedżerowie są skonfigurowani do synchronizacji z Gusto.', - approvalModeWarningTitle: 'Zmienić tryb zatwierdzania?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `Czy na pewno chcesz zmienić tryb zatwierdzania dla tego obszaru roboczego? Dowiedz się więcej o różnych trybach przepływu pracy obsługiwanych przez Gusto w naszej witrynie pomocy.`, - approvalModeWarningConfirm: 'Zmień tryb zatwierdzania', - approvalModes: { - basic: {label: 'Podstawowa akceptacja', description: 'Wszyscy użytkownicy przesyłają wydatki do jednej osoby w celu ich przetworzenia i zatwierdzenia.'}, - manager: {label: 'Zatwierdzenie przez menedżera', description: 'Pracownicy przesyłają raporty do swojego bezpośredniego menedżera skonfigurowanego w Gusto.'}, - custom: {label: 'Niestandardowa akceptacja', description: 'Ręcznie skonfiguruję procesy zatwierdzania w Expensify.'}, - }, - connect: 'Połącz', - connectionDescription: 'Połącz Gusto, aby synchronizować akceptacje pracowników z Twoim miejscem pracy.', - syncNow: 'Synchronizuj teraz', - disconnect: 'Odłącz', - lastSync: (relativeDate: string) => `Ostatnia synchronizacja: ${relativeDate}`, - syncError: 'Nie można połączyć się z Gusto', - disconnectTitle: 'Odłącz Gusto', - disconnectPrompt: 'Czy na pewno chcesz odłączyć Gusto?', syncResults: { title: 'Wyniki synchronizacji Gusto', successTitle: 'Pomyślnie zsynchronizowano Twoje połączenie z Gusto!', @@ -7093,33 +7160,8 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, }), }, }, - merge: { - approvalMode: 'Tryb zatwierdzania', - finalApprover: 'Ostateczny zatwierdzający', - }, zenefits: { title: 'TriNet', - connect: 'Połącz', - syncNow: 'Synchronizuj teraz', - disconnect: 'Odłącz', - lastSync: (relativeDate: string) => `Ostatnia synchronizacja: ${relativeDate}`, - syncError: 'Nie można połączyć się z TriNet', - disconnectTitle: 'Odłącz TriNet', - disconnectPrompt: 'Czy na pewno chcesz odłączyć TriNet?', - connectionDescription: 'Połącz TriNet, żeby utrzymywać zatwierdzenia pracowników w synchronizacji z twoim workspace’em.', - approvalMode: 'Tryb zatwierdzania', - finalApprover: 'Ostateczny zatwierdzający', - notSet: 'Nie ustawiono', - approvalModeDescription: 'Członkowie i menedżerowie są skonfigurowani do synchronizacji z TriNet.', - approvalModeWarningTitle: 'Zmienić tryb zatwierdzania?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `Czy na pewno chcesz zmienić tryb zatwierdzania dla tego workspace? Dowiedz się więcej o różnych trybach workflow obsługiwanych przez TriNet na naszej stronie pomocy.`, - approvalModeWarningConfirm: 'Zmień tryb akceptacji', - approvalModes: { - basic: {label: 'Podstawowa akceptacja', description: 'Wszyscy użytkownicy przesyłają wydatki do jednej osoby w celu ich przetworzenia i zatwierdzenia.'}, - manager: {label: 'Akceptacja przez menedżera', description: 'Pracownicy składają raporty do swojego bezpośredniego przełożonego skonfigurowanego w TriNet.'}, - custom: {label: 'Niestandardowe zatwierdzanie', description: 'Ręcznie skonfiguruję przepływy akceptacji w Expensify.'}, - }, }, }, }, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 7fa52f68cbd1..535659f23f58 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -261,6 +261,7 @@ const translations: TranslationDeepObject = { reset: 'Redefinir', done: 'Concluído', more: 'Mais', + other: 'Outro', debitCard: 'Cartão de débito', bankAccount: 'Conta bancária', personalBankAccount: 'Conta bancária pessoal', @@ -956,8 +957,6 @@ const translations: TranslationDeepObject = { personalSubtitle: 'Carteira', }, }, - assignedCards: 'Seus Cartões Expensify', - assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} restante`, announcements: 'Comunicados', discoverSection: { title: 'Descobrir', @@ -1037,6 +1036,12 @@ const translations: TranslationDeepObject = { setupRules: 'Configurar regras de gasto', inviteAccountant: 'Convide seu contador', }, + yourSpend: { + title: 'Seus gastos', + awaitingApproval: 'Aguardando aprovação', + repaidLast30Days: 'Reembolsado nos últimos 30 dias', + recentTransactions: ({lastFour}: {lastFour: string}) => `Transações recentes • ${lastFour}`, + }, }, allSettingsScreen: { subscription: 'Assinatura', @@ -2527,6 +2532,11 @@ ${amount} para ${merchant} - ${date}`, frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Este cartão foi bloqueado por ${person}. Entre em contato com um administrador para desbloqueá-lo.`, spendRules: 'Regras de gasto', editSpendRules: 'Editar regras de gastos', + cardUnblocked: 'Cartão desbloqueado!', + cardUnblockedDescription: 'Pode ser que você seja solicitado a inserir o seu cartão no leitor na próxima vez que fizer uma compra.', + pinBlocked: 'Seu cartão foi bloqueado devido a tentativas de PIN incorretas.', + unblock: 'Desbloquear', + unblockCard: 'Desbloquear cartão', }, workflowsPage: { workflowTitle: 'Gastos', @@ -2994,6 +3004,8 @@ ${amount} para ${merchant} - ${date}`, title: 'Não foi possível adicionar o e-mail de trabalho', subtitle: (workEmail: string | undefined) => `Não foi possível adicionar ${workEmail}. Tente novamente mais tarde em Configurações ou converse com o Concierge para obter orientação.`, + workAccountClosedSubtitle: + 'A conta de trabalho associada a este e-mail foi encerrada. Entre em contato com o administrador da sua empresa para reativá-la ou inscreva-se com um e-mail diferente.', }, tasks: { testDriveAdminTask: { @@ -6705,6 +6717,57 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Integrações de RH estão disponíveis apenas no plano Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, }, + approvalSubmit: { + title: 'Aprovações', + description: 'Configure centralmente para quem todos os membros enviam relatórios ativando as aprovações.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `As aprovações estão disponíveis nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, + }, + companyCardSubmit: { + title: 'Cartões corporativos', + description: `Traga seu próprio cartão corporativo para o Expensify para obter importação automática, categorização automática, suporte a regras personalizáveis e reconciliação integrada.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `A importação de cartão corporativo está disponível nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, + }, + travelSubmit: { + title: 'Expensify Travel', + description: 'Reserve voos, hotéis, carros e trens com desconto no mundo todo, diretamente pelo Expensify, com relatórios de duty of care e gestão de despesas integrada.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `O Expensify Travel está disponível nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, + }, + roles: { + title: 'Funções', + description: 'Defina funções diferentes para membros diferentes para aumentar ou reduzir a visibilidade e o controle conforme apropriado.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Funções estão disponíveis nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, + }, + payments: { + title: 'Pagamentos', + description: 'Reembolse funcionários diretamente da conta bancária da sua empresa.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Os pagamentos estão disponíveis nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, + }, + accounting: { + title: 'Contabilidade', + description: + 'Sincronize categorias, tags, taxas de imposto e muito mais do seu sistema contábil para o Expensify, além de exportar relatórios de despesas e transações de cartão – sem digitação (ou erros de digitação)!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `O módulo de contabilidade está disponível nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, + }, + expensifyCard: { + title: 'Cartão Expensify', + description: + 'Emita cartões corporativos (incluindo cartões virtuais) diretamente da sua própria conta bancária para ter controle de gastos em tempo real com uma conexão inquebrável e até 2% de cashback!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `O Cartão Expensify está disponível nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, + upgradeButton: 'Fazer upgrade e ativar', + }, + invoicing: { + title: 'Faturamento', + description: 'Crie, envie e acompanhe faturas profissionais, tudo dentro do Expensify. Receba mais rápido com pagamentos integrados e visibilidade em tempo real.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `O faturamento está disponível nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, + }, }, downgrade: { commonFeatures: { @@ -7041,8 +7104,32 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, }, hr: { title: 'RH', + connections: 'Conexões', subtitle: 'Conecte ferramentas de RH e mantenha as aprovações de funcionários em sincronia.', - settingsTitle: 'Configurações do Gusto', + connect: 'Conectar', + syncNow: 'Sincronizar agora', + disconnect: 'Desconectar', + disconnectTitle: (providerName: string) => `Desconectar ${providerName}`, + disconnectPrompt: (providerName: string) => `Tem certeza de que deseja desconectar ${providerName}?`, + lastSync: (relativeDate: string) => `Última sincronização ${relativeDate}`, + syncError: (providerName: string) => `Não é possível conectar ao ${providerName}`, + connectionDescription: (providerName: string) => `Conecte ${providerName} para manter as aprovações de funcionários sincronizadas com seu workspace.`, + approvalMode: 'Modo de aprovação', + finalApprover: 'Aprovador final', + notSet: 'Não definido', + approvalModeDescription: (providerName: string) => `Membros e gerentes estão configurados para sincronizar com ${providerName}.`, + approvalModeWarningTitle: 'Alterar modo de aprovação?', + approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => + `Tem certeza de que deseja alterar o modo de aprovação deste workspace? Saiba mais sobre os diferentes modos de fluxo de trabalho com ${providerName} em nosso site de ajuda.`, + approvalModeWarningConfirm: 'Alterar modo de aprovação', + approvalModes: { + basic: {label: 'Aprovação básica', description: 'Todos os usuários enviam para uma única pessoa para processamento e aprovação.'}, + manager: { + label: 'Aprovação do gerente', + description: (providerName: string) => `Os funcionários enviam relatórios ao gestor direto configurado no ${providerName}.`, + }, + custom: {label: 'Aprovação personalizada', description: 'Vou configurar manualmente os fluxos de aprovação no Expensify.'}, + }, syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -7066,27 +7153,6 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, }, gusto: { title: 'Gusto', - approvalMode: 'Modo de aprovação', - finalApprover: 'Aprovador final', - notSet: 'Não definido', - approvalModeDescription: 'Membros e gerentes estão configurados para sincronizar com o Gusto.', - approvalModeWarningTitle: 'Alterar modo de aprovação?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `Tem certeza de que deseja alterar o modo de aprovação deste workspace? Saiba mais sobre os diferentes modos de fluxo de trabalho com Gusto em nosso site de ajuda.`, - approvalModeWarningConfirm: 'Alterar modo de aprovação', - approvalModes: { - basic: {label: 'Aprovação básica', description: 'Todos os usuários enviam para uma única pessoa para processamento e aprovação.'}, - manager: {label: 'Aprovação do gerente', description: 'Os funcionários enviam relatórios ao gestor direto configurado no Gusto.'}, - custom: {label: 'Aprovação personalizada', description: 'Vou configurar manualmente os fluxos de aprovação no Expensify.'}, - }, - connect: 'Conectar', - connectionDescription: 'Conecte o Gusto para manter as aprovações de funcionários sincronizadas com seu workspace.', - syncNow: 'Sincronizar agora', - disconnect: 'Desconectar', - lastSync: (relativeDate: string) => `Última sincronização ${relativeDate}`, - syncError: 'Não é possível conectar ao Gusto', - disconnectTitle: 'Desconectar Gusto', - disconnectPrompt: 'Tem certeza de que deseja desconectar o Gusto?', syncResults: { title: 'Resultados da sincronização com Gusto', successTitle: 'Sua conexão com o Gusto foi sincronizada com sucesso!', @@ -7099,33 +7165,8 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, }), }, }, - merge: { - approvalMode: 'Modo de aprovação', - finalApprover: 'Aprovador final', - }, zenefits: { title: 'TriNet', - connect: 'Conectar', - syncNow: 'Sincronizar agora', - disconnect: 'Desconectar', - lastSync: (relativeDate: string) => `Última sincronização ${relativeDate}`, - syncError: 'Não é possível conectar ao TriNet', - disconnectTitle: 'Desconectar TriNet', - disconnectPrompt: 'Tem certeza de que quer desconectar a TriNet?', - connectionDescription: 'Conecte o TriNet para manter as aprovações de funcionários sincronizadas com o seu espaço de trabalho.', - approvalMode: 'Modo de aprovação', - finalApprover: 'Aprovador final', - notSet: 'Não definido', - approvalModeDescription: 'Os membros e gerentes estão configurados para sincronizar com o TriNet.', - approvalModeWarningTitle: 'Alterar modo de aprovação?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `Tem certeza de que deseja alterar o modo de aprovação deste espaço de trabalho? Saiba mais sobre os diferentes modos de fluxo de trabalho habilitados pelo TriNet no nosso site de ajuda.`, - approvalModeWarningConfirm: 'Alterar modo de aprovação', - approvalModes: { - basic: {label: 'Aprovação básica', description: 'Todos os usuários enviam para uma única pessoa para processamento e aprovação.'}, - manager: {label: 'Aprovação do gerente', description: 'Os funcionários enviam relatórios para o gerente direto configurado no TriNet.'}, - custom: {label: 'Aprovação personalizada', description: 'Vou configurar manualmente fluxos de aprovação no Expensify.'}, - }, }, }, }, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 7281eaf7b8d6..3c9e8607e50e 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -261,6 +261,7 @@ const translations: TranslationDeepObject = { reset: '重置', done: '完成', more: '更多', + other: '其他', debitCard: '借记卡', bankAccount: '银行账户', personalBankAccount: '个人银行账户', @@ -924,8 +925,6 @@ const translations: TranslationDeepObject = { personalSubtitle: '钱包', }, }, - assignedCards: '你的 Expensify 卡', - assignedCardsRemaining: ({amount}: {amount: string}) => `剩余 ${amount}`, announcements: '公告', discoverSection: { title: '发现', @@ -1005,6 +1004,7 @@ const translations: TranslationDeepObject = { setupRules: '设置消费规则', inviteAccountant: '邀请你的会计', }, + yourSpend: {title: '您的支出', awaitingApproval: '等待审批', repaidLast30Days: '过去30天内已偿还', recentTransactions: ({lastFour}: {lastFour: string}) => `最近交易 • ${lastFour}`}, }, allSettingsScreen: { subscription: '订阅', @@ -2466,6 +2466,11 @@ ${amount},商户:${merchant} - 日期:${date}`, frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `此卡已被${person}冻结。请联系管理员解冻。`, spendRules: '支出规则', editSpendRules: '编辑支出规则', + cardUnblocked: '卡已解除冻结!', + cardUnblockedDescription: '下次消费时,系统可能会提示你将银行卡插入读卡器。', + pinBlocked: '由于多次输入错误 PIN,您的银行卡已被停用。', + unblock: '取消屏蔽', + unblockCard: '解除卡片冻结', }, workflowsPage: { workflowTitle: '支出', @@ -2924,6 +2929,7 @@ ${amount},商户:${merchant} - 日期:${date}`, mergeBlockScreen: { title: '无法添加工作邮箱', subtitle: (workEmail: string | undefined) => `我们无法添加 ${workEmail}。请稍后在设置中重试,或与 Concierge 聊天以获取指导。`, + workAccountClosedSubtitle: '与此邮箱关联的工作账户已被关闭。请联系您公司的管理员以重新激活该账户,或使用其他邮箱注册。', }, tasks: { testDriveAdminTask: { @@ -6537,6 +6543,55 @@ ${reportName} onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `人力资源集成功能仅在 Control 方案中提供,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, }, + approvalSubmit: { + title: '审批', + description: '启用审批功能,以集中配置所有成员向谁提交。', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `审批功能适用于 Collect 和 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, + }, + companyCardSubmit: { + title: '公司卡枚', + description: `将你自己的公司卡片连接到 Expensify,可享受自动导入、自动分类、自定义规则支持以及集成对账功能。`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `公司信用卡导入功能适用于 Collect 和 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, + }, + travelSubmit: { + title: 'Expensify 差旅', + description: '通过 Expensify 在全球预订折扣机票、酒店、租车和火车,并享受差旅关怀报告和集成费用管理。', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Expensify Travel 适用于 Collect 和 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, + }, + roles: { + title: '角色', + description: '为不同成员分配不同角色,以根据需要提高或降低可见性和控制权。', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `角色可在 Collect 和 Control 方案中使用,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, + }, + payments: { + title: '付款', + description: '直接从您的企业银行账户向员工报销。', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `付款功能适用于 Collect 和 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, + }, + accounting: { + title: '会计', + description: '从您的会计系统同步类别、标签、税率等信息到 Expensify,并导出费用报表和卡片交易记录——全程无需手动输入,也不用担心输入错误!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `会计功能适用于 Collect 和 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, + }, + expensifyCard: { + title: 'Expensify 卡', + description: '直接从您自己的银行账户发放公司卡(包括虚拟卡),通过牢不可破的连接实现实时消费管控,并享受最高 2% 返现!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Expensify 卡适用于 Collect 和 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, + upgradeButton: '升级并启用', + }, + invoicing: { + title: '开具发票', + description: '在 Expensify 中创建、发送并跟踪专业发票。通过集成付款和实时可见性,让收款更快捷。', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `开票功能适用于 Collect 和 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, + }, }, downgrade: { commonFeatures: { @@ -6866,8 +6921,32 @@ ${reportName} }, hr: { title: '人力资源', + connections: '连接', subtitle: '连接人力资源工具,保持员工审批同步。', - settingsTitle: 'Gusto 设置', + connect: '连接', + syncNow: '立即同步', + disconnect: '断开连接', + disconnectTitle: (providerName: string) => `断开 ${providerName}`, + disconnectPrompt: (providerName: string) => `确定要断开 ${providerName} 吗?`, + lastSync: (relativeDate: string) => `上次同步 ${relativeDate}`, + syncError: (providerName: string) => `无法连接到 ${providerName}`, + connectionDescription: (providerName: string) => `连接 ${providerName},以在您的工作区中同步员工审批。`, + approvalMode: '审批模式', + finalApprover: '最终审批人', + notSet: '未设置', + approvalModeDescription: (providerName: string) => `成员和管理员已设置为与 ${providerName} 同步。`, + approvalModeWarningTitle: '更改审批模式?', + approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => + `您确定要更改此工作区的审批模式吗?在我们的帮助网站中了解更多关于不同 ${providerName} 启用的工作流模式的信息。`, + approvalModeWarningConfirm: '更改审批模式', + approvalModes: { + basic: {label: '基本审批', description: '所有用户都提交给同一个人进行处理和审批。'}, + manager: { + label: '经理审批', + description: (providerName: string) => `员工会将报销单提交给在 ${providerName} 中配置的直属经理。`, + }, + custom: {label: '自定义审批', description: '我将在 Expensify 中手动设置审批工作流程。'}, + }, syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -6891,27 +6970,6 @@ ${reportName} }, gusto: { title: 'Gusto', - approvalMode: '审批模式', - finalApprover: '最终审批人', - notSet: '未设置', - approvalModeDescription: '成员和管理员已设置为与 Gusto 同步。', - approvalModeWarningTitle: '更改审批模式?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `您确定要更改此工作区的审批模式吗?在我们的帮助网站中了解更多关于不同 Gusto 启用的工作流模式的信息。`, - approvalModeWarningConfirm: '更改审批模式', - approvalModes: { - basic: {label: '基础审批', description: '所有用户都提交给同一个人进行处理和审批。'}, - manager: {label: '经理审批', description: '员工会将报销单提交给在 Gusto 中配置的直属经理。'}, - custom: {label: '自定义审批', description: '我将在 Expensify 中手动设置审批工作流程。'}, - }, - connect: '连接', - connectionDescription: '连接 Gusto,以在您的工作区中同步员工审批。', - syncNow: '立即同步', - disconnect: '断开连接', - lastSync: (relativeDate: string) => `上次同步时间:${relativeDate}`, - syncError: '无法连接到 Gusto', - disconnectTitle: '断开 Gusto', - disconnectPrompt: '确定要断开与 Gusto 的连接吗?', syncResults: { title: 'Gusto 同步结果', successTitle: '已成功同步你的 Gusto 连接!', @@ -6924,33 +6982,8 @@ ${reportName} }), }, }, - merge: { - approvalMode: '审批模式', - finalApprover: '最终审批人', - }, zenefits: { title: 'TriNet', - connect: '连接', - syncNow: '立即同步', - disconnect: '断开连接', - lastSync: (relativeDate: string) => `上次同步时间:${relativeDate}`, - syncError: '无法连接到 TriNet', - disconnectTitle: '断开 TriNet 连接', - disconnectPrompt: '确定要断开 TriNet 吗?', - connectionDescription: '连接 TriNet,将员工审批与你的工作区保持同步。', - approvalMode: '审批模式', - finalApprover: '最终审批人', - notSet: '未设置', - approvalModeDescription: '成员和管理者已设置为与 TriNet 同步。', - approvalModeWarningTitle: '更改审批模式?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `您确定要更改此工作区的审批模式吗?在我们的帮助网站中了解更多关于不同 TriNet 启用的工作流模式的信息。`, - approvalModeWarningConfirm: '更改审批模式', - approvalModes: { - basic: {label: '基础审批', description: '所有用户都提交给同一个人进行处理和审批。'}, - manager: {label: '经理审批', description: '员工会将报销报告提交给在 TriNet 中配置的直属经理。'}, - custom: {label: '自定义审批', description: '我会在 Expensify 中手动设置审批流程。'}, - }, }, }, }, From 8a51509d89ddccfcc5fa3359ee3b293032aa2113 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Thu, 21 May 2026 07:58:44 +0000 Subject: [PATCH 19/31] Remove all unrelated en.ts changes, keep only duplicateFeedModal keys Co-authored-by: Joel Davies --- src/languages/en.ts | 162 +++++++++++++++++++++++++------------------- 1 file changed, 93 insertions(+), 69 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index c883abf3cd7e..b517703ee7b4 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -270,6 +270,7 @@ const translations = { // @context Status or button indicating that an action or process has been completed. Should reflect completion. done: 'Done', more: 'More', + other: 'Other', debitCard: 'Debit card', bankAccount: 'Bank account', personalBankAccount: 'Personal bank account', @@ -1017,8 +1018,12 @@ const translations = { other: (pluralCount: number) => `Time remaining: ${pluralCount} days`, }), }, - assignedCards: 'Your Expensify Cards', - assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} remaining`, + yourSpend: { + title: 'Your spend', + awaitingApproval: 'Awaiting approval', + repaidLast30Days: 'Repaid in the last 30 days', + recentTransactions: ({lastFour}: {lastFour: string}) => `Recent transactions • ${lastFour}`, + }, announcements: 'Announcements', discoverSection: { title: 'Discover', @@ -2578,6 +2583,11 @@ const translations = { pinChanged: 'PIN changed!', pinChangedHeader: 'PIN changed', pinChangedDescription: "You're all set to use your PIN now.", + cardUnblocked: 'Card unblocked!', + cardUnblockedDescription: 'You may be prompted to insert your card into the reader the next time you make a purchase.', + pinBlocked: 'Your card was blocked due to incorrect PIN entries.', + unblock: 'Unblock', + unblockCard: 'Unblock card', changePin: 'Change PIN', changePinAtATM: 'Change your PIN at any ATM', changePinAtATMDescription: 'This is required in your region. Reach out to Concierge if you have any questions.', @@ -3072,6 +3082,7 @@ const translations = { mergeBlockScreen: { title: 'Couldn’t add work email', subtitle: (workEmail: string | undefined) => `We couldn’t add ${workEmail}. Please try again later in Settings or chat with Concierge for guidance.`, + workAccountClosedSubtitle: 'The work account associated with this email is closed. Please contact your company admin to reactivate it, or sign up with a different email.', }, tasks: { testDriveAdminTask: { @@ -6437,8 +6448,32 @@ const translations = { }, hr: { title: 'HR', + connections: 'Connections', subtitle: 'Connect HR tools and keep employee approvals in sync.', - settingsTitle: 'Gusto settings', + connect: 'Connect', + syncNow: 'Sync now', + disconnect: 'Disconnect', + disconnectTitle: (providerName: string) => `Disconnect ${providerName}`, + disconnectPrompt: (providerName: string) => `Are you sure you want to disconnect ${providerName}?`, + lastSync: (relativeDate: string) => `Last synced ${relativeDate}`, + syncError: (providerName: string) => `Can't connect to ${providerName}`, + connectionDescription: (providerName: string) => `Connect ${providerName} to keep employee approvals in sync with your workspace.`, + approvalMode: 'Approval mode', + finalApprover: 'Final approver', + notSet: 'Not set', + approvalModeDescription: (providerName: string) => `Members and managers are set up to sync with ${providerName}.`, + approvalModeWarningTitle: 'Change approval mode?', + approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => + `Are you sure you would like to change the approval mode for this workspace? Learn more about the different ${providerName}-enabled workflow modes in our help site.`, + approvalModeWarningConfirm: 'Change approval mode', + approvalModes: { + basic: {label: 'Basic approval', description: 'All users submit to a single person for processing and approval.'}, + manager: { + label: 'Manager approval', + description: (providerName: string) => `Employees submit reports to their direct manager configured in ${providerName}.`, + }, + custom: {label: 'Custom approval', description: "I'll manually setup approval workflows in Expensify."}, + }, syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -6453,6 +6488,8 @@ const translations = { return 'Loading data from TriNet'; case 'zenefitsSyncProvisioning': return 'Provisioning employees in policy'; + case 'mergeHRSyncTitle': + return 'Synchronizing HR Employees'; case 'jobDone': return 'Waiting for imported data to load'; default: { @@ -6462,36 +6499,6 @@ const translations = { }, gusto: { title: 'Gusto', - connect: 'Connect', - syncNow: 'Sync now', - disconnect: 'Disconnect', - lastSync: (relativeDate: string) => `Last synced ${relativeDate}`, - syncError: "Can't connect to Gusto", - disconnectTitle: 'Disconnect Gusto', - disconnectPrompt: 'Are you sure you want to disconnect Gusto?', - connectionDescription: 'Connect Gusto to keep employee approvals in sync with your workspace.', - approvalMode: 'Approval mode', - finalApprover: 'Final approver', - notSet: 'Not set', - approvalModeDescription: 'Members and managers are set up to sync with Gusto.', - approvalModeWarningTitle: 'Change approval mode?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `Are you sure you would like to change the approval mode for this workspace? Learn more about the different Gusto-enabled workflow modes in our help site.`, - approvalModeWarningConfirm: 'Change approval mode', - approvalModes: { - basic: { - label: 'Basic approval', - description: 'All users submit to a single person for processing and approval.', - }, - manager: { - label: 'Manager approval', - description: 'Employees submit reports to their direct manager configured in Gusto.', - }, - custom: { - label: 'Custom approval', - description: 'I’ll manually setup approval workflows in Expensify.', - }, - }, syncResults: { title: 'Gusto sync results', successTitle: 'Successfully synced your Gusto connection!', @@ -6504,42 +6511,8 @@ const translations = { }), }, }, - merge: { - approvalMode: 'Approval mode', - finalApprover: 'Final approver', - }, zenefits: { title: 'TriNet', - connect: 'Connect', - syncNow: 'Sync now', - disconnect: 'Disconnect', - lastSync: (relativeDate: string) => `Last synced ${relativeDate}`, - syncError: "Can't connect to TriNet", - disconnectTitle: 'Disconnect TriNet', - disconnectPrompt: 'Are you sure you want to disconnect TriNet?', - connectionDescription: 'Connect TriNet to keep employee approvals in sync with your workspace.', - approvalMode: 'Approval mode', - finalApprover: 'Final approver', - notSet: 'Not set', - approvalModeDescription: 'Members and managers are set up to sync with TriNet.', - approvalModeWarningTitle: 'Change approval mode?', - approvalModeWarningPrompt: (helpSiteURL: string) => - `Are you sure you would like to change the approval mode for this workspace? Learn more about the different TriNet-enabled workflow modes in our help site.`, - approvalModeWarningConfirm: 'Change approval mode', - approvalModes: { - basic: { - label: 'Basic approval', - description: 'All users submit to a single person for processing and approval.', - }, - manager: { - label: 'Manager approval', - description: 'Employees submit reports to their direct manager configured in TriNet.', - }, - custom: { - label: 'Custom approval', - description: "I'll manually setup approval workflows in Expensify.", - }, - }, }, }, export: { @@ -6738,6 +6711,12 @@ const translations = { onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Advanced approvals are only available on the Control plan, which starts at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, }, + approvalSubmit: { + title: 'Approvals', + description: 'Centrally configure who all members submit to by enabling approvals.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Approvals are available on the Collect and Control plans, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, + }, categories: { title: 'Categories', description: 'Categories allow you to track and organize spend. Use our default categories or add your own.', @@ -6768,6 +6747,12 @@ const translations = { onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `This is only available on the Control plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, }, + companyCardSubmit: { + title: 'Company cards', + description: `Bring your own company card to Expensify to get automatic import, automatic categorization, customizable rule support, and integrated reconciliation.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Company card import is available on the Collect and Control plans, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, + }, rules: { title: 'Rules', description: `Rules run in the background and keep your spend under control so you don't have to sweat the small stuff.\n\nRequire expense details like receipts and descriptions, set limits and defaults, and automate approvals and payments – all in one place.`, @@ -6794,6 +6779,12 @@ const translations = { onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Travel is available on the Collect plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, }, + travelSubmit: { + title: 'Expensify Travel', + description: 'Book discounted flights, hotels, cars, and trains globally from within Expensify, with duty of care reporting and integrated expense management.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Expensify Travel is available on the Collect and Control plans, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, + }, reports: { title: 'Reports', description: 'Reports allow you to group expenses for easier tracking and organization.', @@ -6825,6 +6816,39 @@ const translations = { onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Multiple approval levels are only available on the Control plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, }, + roles: { + title: 'Roles', + description: 'Assign different roles to different members to increase or decrease visibility and control as appropriate.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Roles are available on the Collect and Control plans, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, + }, + payments: { + title: 'Payments', + description: 'Reimburse employees directly from your business bank account.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Payments are available on the Collect and Control plans, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, + }, + accounting: { + title: 'Accounting', + description: + 'Sync categories, tags, tax rates, and more from your accounting system to Expensify, as well as export expense reports and card transactions – no typing (or typos) involved!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Accounting is available on the Collect and Control plans, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, + }, + expensifyCard: { + title: 'Expensify Card', + description: + 'Issue company cards (including virtual cards) directly from your own bank account to get realtime spend control with an unbreakable connection, and up to 2% cashback!', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `The Expensify Card is available on the Collect and Control plans, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, + upgradeButton: 'Upgrade and enable', + }, + invoicing: { + title: 'Invoicing', + description: 'Create, send, and track professional invoices, all inside Expensify. Get paid faster with integrated payments and real-time visibility.', + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Invoicing is available on the Collect and Control plans, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, + }, pricing: { perActiveMember: 'per active member per month.', perMember: 'per member per month.', @@ -7173,8 +7197,8 @@ const translations = { description: 'For employees looking to submit expenses to their employer.', }, }, - description: "Choose a plan that's right for you. For a detailed list of features and pricing, check out our", - subscriptionLink: 'plan types and pricing help page', + description: "Choose a plan that's right for you.", + subscriptionLink: 'Learn more', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `You've committed to 1 active member on the Control plan until your annual subscription ends on ${annualSubscriptionEndDate}. You can switch to pay-per-use subscription and downgrade to the Collect plan starting ${annualSubscriptionEndDate} by disabling auto-renew in`, other: `You've committed to ${count} active members on the Control plan until your annual subscription ends on ${annualSubscriptionEndDate}. You can switch to pay-per-use subscription and downgrade to the Collect plan starting ${annualSubscriptionEndDate} by disabling auto-renew in`, From d9cfccf6bef0c55eb4f77bdf56bd15d6f4560595 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Thu, 21 May 2026 07:59:43 +0000 Subject: [PATCH 20/31] Reset language files to PR base, keep only duplicateFeedModal in en.ts Co-authored-by: Joel Davies --- src/languages/de.ts | 144 ++++++++++++---------------------- src/languages/en.ts | 162 +++++++++++++++++---------------------- src/languages/es.ts | 154 +++++++++++++++---------------------- src/languages/fr.ts | 151 ++++++++++++------------------------ src/languages/it.ts | 145 ++++++++++++----------------------- src/languages/ja.ts | 142 ++++++++++++---------------------- src/languages/nl.ts | 143 ++++++++++++---------------------- src/languages/pl.ts | 144 ++++++++++++---------------------- src/languages/pt-BR.ts | 143 ++++++++++++---------------------- src/languages/zh-hans.ts | 135 ++++++++++++-------------------- 10 files changed, 537 insertions(+), 926 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index c77a6ba1bd60..8ac304f70aad 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -261,7 +261,6 @@ const translations: TranslationDeepObject = { reset: 'Zurücksetzen', done: 'Fertig', more: 'Mehr', - other: 'Andere', debitCard: 'Debitkarte', bankAccount: 'Bankkonto', personalBankAccount: 'Persönliches Bankkonto', @@ -958,6 +957,8 @@ const translations: TranslationDeepObject = { personalSubtitle: 'Geldbörse', }, }, + assignedCards: 'Ihre Expensify Karten', + assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} verbleibend`, announcements: 'Ankündigungen', discoverSection: { title: 'Entdecken', @@ -1037,12 +1038,6 @@ const translations: TranslationDeepObject = { other: (pluralCount: number) => `Verbleibende Zeit: ${pluralCount} Tage`, }), }, - yourSpend: { - title: 'Ihre Ausgaben', - awaitingApproval: 'Ausstehende Genehmigung', - repaidLast30Days: 'In den letzten 30 Tagen zurückgezahlt', - recentTransactions: ({lastFour}: {lastFour: string}) => `Aktuelle Transaktionen • ${lastFour}`, - }, }, allSettingsScreen: { subscription: 'Abonnement', @@ -2545,11 +2540,6 @@ ${amount} für ${merchant} – ${date}`, frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Diese Karte wurde von ${person} gesperrt. Bitte kontaktiere einen Admin, um sie zu entsperren.`, spendRules: 'Ausgaberichtlinien', editSpendRules: 'Ausgaberegeln bearbeiten', - cardUnblocked: 'Karte entsperrt!', - cardUnblockedDescription: 'Möglicherweise werden Sie beim nächsten Einkauf aufgefordert, Ihre Karte in das Lesegerät einzustecken.', - pinBlocked: 'Ihre Karte wurde aufgrund falscher PIN-Eingaben gesperrt.', - unblock: 'Entsperren', - unblockCard: 'Karte entsperren', }, workflowsPage: { workflowTitle: 'Ausgaben', @@ -3020,8 +3010,6 @@ ${amount} für ${merchant} – ${date}`, title: 'Arbeits-E-Mail konnte nicht hinzugefügt werden', subtitle: (workEmail: string | undefined) => `Wir konnten ${workEmail} nicht hinzufügen. Bitte versuche es später in den Einstellungen erneut oder chatte mit Concierge, um Unterstützung zu erhalten.`, - workAccountClosedSubtitle: - 'Das Arbeitskonto, das mit dieser E-Mail-Adresse verknüpft ist, wurde geschlossen. Bitte wenden Sie sich an Ihre Unternehmensadministratorin bzw. Ihren Unternehmensadministrator, um es zu reaktivieren, oder registrieren Sie sich mit einer anderen E-Mail-Adresse.', }, tasks: { testDriveAdminTask: { @@ -6754,58 +6742,6 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `HR-Integrationen sind nur im Control-Tarif verfügbar, ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, }, - approvalSubmit: { - title: 'Genehmigungen', - description: 'Konfigurieren Sie zentral, an wen alle Mitglieder einreichen, indem Sie Genehmigungen aktivieren.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Genehmigungen sind in den Collect- und Control-Tarifen verfügbar, beginnend bei ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, - }, - companyCardSubmit: { - title: 'Firmenkarten', - description: `Bringe deine eigene Firmenkarte zu Expensify, um automatische Importe, automatische Kategorisierung, anpassbare Regeln und integrierte Abstimmung zu erhalten.`, - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Der Import von Firmenkreditkarten ist in den Collect- und Control-Tarifen verfügbar, beginnend bei ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, - }, - travelSubmit: { - title: 'Expensify Travel', - description: 'Buche weltweit vergünstigte Flüge, Hotels, Autos und Züge direkt in Expensify – mit Duty-of-Care-Berichten und integrierter Spesenverwaltung.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Expensify Travel ist in den Collect- und Control-Tarifen verfügbar, beginnend bei ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, - }, - roles: { - title: 'Rollen', - description: 'Weise verschiedenen Mitgliedern unterschiedliche Rollen zu, um Sichtbarkeit und Kontrolle bei Bedarf zu erhöhen oder zu verringern.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Rollen sind in den Collect- und Control-Tarifen verfügbar, beginnend ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, - }, - payments: { - title: 'Zahlungen', - description: 'Erstatte Mitarbeitenden direkt von deinem Geschäftskonto.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Zahlungen sind in den Collect- und Control-Tarifen verfügbar, ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, - }, - accounting: { - title: 'Buchhaltung', - description: - 'Synchronisiere Kategorien, Tags, Steuersätze und mehr aus deinem Buchhaltungssystem mit Expensify und exportiere Spesenberichte sowie Kartentransaktionen – ganz ohne Tippen (oder Tippfehler)!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Buchhaltung ist in den Tarifen Collect und Control verfügbar, ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, - }, - expensifyCard: { - title: 'Expensify Card', - description: - 'Gib Firmenkarten (einschließlich virtueller Karten) direkt von deinem eigenen Bankkonto aus, um in Echtzeit die Ausgaben zu kontrollieren – mit einer untrennbaren Verbindung und bis zu 2 % Cashback!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Die Expensify Card ist in den Collect- und Control-Tarifen verfügbar, ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, - upgradeButton: 'Upgrade durchführen und aktivieren', - }, - invoicing: { - title: 'Rechnungsstellung', - description: - 'Erstelle, versende und verfolge professionelle Rechnungen – alles in Expensify. Erhalte dein Geld schneller mit integrierten Zahlungen und vollständiger Transparenz in Echtzeit.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Die Rechnungsstellung ist in den Collect- und Control-Tarifen verfügbar, beginnend ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, - }, }, downgrade: { commonFeatures: { @@ -7132,8 +7068,8 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc description: 'Für Mitarbeiter, die Ausgaben bei ihrem Arbeitgeber einreichen möchten.', }, }, - description: 'Wählen Sie das passende Abo für Sie.', - subscriptionLink: 'Mehr erfahren', + description: 'Wähle ein passendes Abo für dich. Eine detaillierte Liste der Funktionen und Preise findest du in unserem', + subscriptionLink: 'Hilfeseite zu Plantypen und Preisen', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `Sie haben sich bis zum Ende Ihres Jahresabonnements am ${annualSubscriptionEndDate} zu 1 aktivem Mitglied im Control-Tarif verpflichtet. Sie können ab dem ${annualSubscriptionEndDate} zu einem nutzungsbasierten Abonnement wechseln und in den Collect-Tarif herabstufen, indem Sie die automatische Verlängerung in`, other: `Sie haben sich bis zum Ende Ihres Jahresabonnements am ${annualSubscriptionEndDate} zu ${count} aktiven Mitgliedern im Control-Tarif verpflichtet. Ab dem ${annualSubscriptionEndDate} können Sie durch Deaktivieren der automatischen Verlängerung in ein nutzungsabhängiges Abonnement wechseln und auf den Collect-Tarif herabstufen in`, @@ -7142,32 +7078,8 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc }, hr: { title: 'Personalwesen', - connections: 'Verbindungen', subtitle: 'HR-Tools verbinden und Mitarbeitergenehmigungen synchron halten.', - connect: 'Verbinden', - syncNow: 'Jetzt synchronisieren', - disconnect: 'Trennen', - disconnectTitle: (providerName: string) => `${providerName} trennen`, - disconnectPrompt: (providerName: string) => `Möchten Sie ${providerName} wirklich trennen?`, - lastSync: (relativeDate: string) => `Zuletzt synchronisiert ${relativeDate}`, - syncError: (providerName: string) => `Verbindung zu ${providerName} nicht möglich`, - connectionDescription: (providerName: string) => `Verbinden Sie ${providerName}, um Mitarbeitergenehmigungen mit Ihrem Workspace zu synchronisieren.`, - approvalMode: 'Genehmigungsmodus', - finalApprover: 'Endgültige:r Genehmiger:in', - notSet: 'Nicht festgelegt', - approvalModeDescription: (providerName: string) => `Mitglieder und Manager sind für die Synchronisation mit ${providerName} eingerichtet.`, - approvalModeWarningTitle: 'Genehmigungsmodus ändern?', - approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => - `Möchten Sie den Genehmigungsmodus für diesen Workspace wirklich ändern? Erfahren Sie mehr über die verschiedenen ${providerName}-aktivierten Workflow-Modi auf unserer Hilfeseite.`, - approvalModeWarningConfirm: 'Genehmigungsmodus ändern', - approvalModes: { - basic: {label: 'Einfache Genehmigung', description: 'Alle Benutzer reichen zur Bearbeitung und Genehmigung bei einer einzigen Person ein.'}, - manager: { - label: 'Manager-Genehmigung', - description: (providerName: string) => `Mitarbeitende reichen Berichte bei ihrer in ${providerName} hinterlegten direkten Führungskraft ein.`, - }, - custom: {label: 'Benutzerdefinierte Genehmigung', description: 'Ich richte Genehmigungs-Workflows in Expensify manuell ein.'}, - }, + settingsTitle: 'Gusto-Einstellungen', syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -7191,6 +7103,27 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc }, gusto: { title: 'Gusto', + approvalMode: 'Genehmigungsmodus', + finalApprover: 'Endgültige:r Genehmiger:in', + notSet: 'Nicht festgelegt', + approvalModeDescription: 'Mitglieder und Manager sind für die Synchronisation mit Gusto eingerichtet.', + approvalModeWarningTitle: 'Genehmigungsmodus ändern?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `Möchten Sie den Genehmigungsmodus für diesen Workspace wirklich ändern? Erfahren Sie mehr über die verschiedenen Gusto-aktivierten Workflow-Modi auf unserer Hilfeseite.`, + approvalModeWarningConfirm: 'Genehmigungsmodus ändern', + approvalModes: { + basic: {label: 'Einfache Genehmigung', description: 'Alle Benutzer reichen zur Bearbeitung und Genehmigung bei einer einzigen Person ein.'}, + manager: {label: 'Managergenehmigung', description: 'Mitarbeitende reichen Berichte bei ihrer in Gusto hinterlegten direkten Führungskraft ein.'}, + custom: {label: 'Benutzerdefinierte Genehmigung', description: 'Ich richte Genehmigungs-Workflows in Expensify manuell ein.'}, + }, + connect: 'Verbinden', + connectionDescription: 'Verbinde Gusto, um Mitarbeitergenehmigungen mit deinem Workspace zu synchronisieren.', + syncNow: 'Jetzt synchronisieren', + disconnect: 'Trennen', + lastSync: (relativeDate: string) => `Zuletzt synchronisiert ${relativeDate}`, + syncError: 'Verbindung zu Gusto nicht möglich', + disconnectTitle: 'Gusto trennen', + disconnectPrompt: 'Bist du sicher, dass du Gusto trennen möchtest?', syncResults: { title: 'Gusto-Synchronisierungsergebnisse', successTitle: 'Ihre Gusto-Verbindung wurde erfolgreich synchronisiert!', @@ -7203,8 +7136,33 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc }), }, }, + merge: { + approvalMode: 'Genehmigungsmodus', + finalApprover: 'Endgültige:r Genehmiger:in', + }, zenefits: { title: 'TriNet', + connect: 'Verbinden', + syncNow: 'Jetzt synchronisieren', + disconnect: 'Verbindung trennen', + lastSync: (relativeDate: string) => `Zuletzt synchronisiert ${relativeDate}`, + syncError: 'Verbindung zu TriNet nicht möglich', + disconnectTitle: 'TriNet trennen', + disconnectPrompt: 'Sind Sie sicher, dass Sie die Verbindung zu TriNet trennen möchten?', + connectionDescription: 'Verbinden Sie TriNet, um Mitarbeitergenehmigungen mit Ihrem Workspace zu synchronisieren.', + approvalMode: 'Genehmigungsmodus', + finalApprover: 'Letzte genehmigende Person', + notSet: 'Nicht festgelegt', + approvalModeDescription: 'Mitglieder und Manager sind für die Synchronisierung mit TriNet eingerichtet.', + approvalModeWarningTitle: 'Genehmigungsmodus ändern?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `Sind Sie sicher, dass Sie den Genehmigungsmodus für diesen Workspace ändern möchten? Erfahren Sie mehr über die verschiedenen TriNet-fähigen Workflow-Modi auf unserer Hilfeseite.`, + approvalModeWarningConfirm: 'Genehmigungsmodus ändern', + approvalModes: { + basic: {label: 'Einfache Genehmigung', description: 'Alle Benutzer reichen an eine einzige Person zur Bearbeitung und Genehmigung ein.'}, + manager: {label: 'Managergenehmigung', description: 'Mitarbeitende reichen Berichte bei ihrer in TriNet hinterlegten direkten Führungskraft ein.'}, + custom: {label: 'Benutzerdefinierte Genehmigung', description: 'Ich richte Genehmigungsabläufe in Expensify manuell ein.'}, + }, }, }, }, diff --git a/src/languages/en.ts b/src/languages/en.ts index b517703ee7b4..c883abf3cd7e 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -270,7 +270,6 @@ const translations = { // @context Status or button indicating that an action or process has been completed. Should reflect completion. done: 'Done', more: 'More', - other: 'Other', debitCard: 'Debit card', bankAccount: 'Bank account', personalBankAccount: 'Personal bank account', @@ -1018,12 +1017,8 @@ const translations = { other: (pluralCount: number) => `Time remaining: ${pluralCount} days`, }), }, - yourSpend: { - title: 'Your spend', - awaitingApproval: 'Awaiting approval', - repaidLast30Days: 'Repaid in the last 30 days', - recentTransactions: ({lastFour}: {lastFour: string}) => `Recent transactions • ${lastFour}`, - }, + assignedCards: 'Your Expensify Cards', + assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} remaining`, announcements: 'Announcements', discoverSection: { title: 'Discover', @@ -2583,11 +2578,6 @@ const translations = { pinChanged: 'PIN changed!', pinChangedHeader: 'PIN changed', pinChangedDescription: "You're all set to use your PIN now.", - cardUnblocked: 'Card unblocked!', - cardUnblockedDescription: 'You may be prompted to insert your card into the reader the next time you make a purchase.', - pinBlocked: 'Your card was blocked due to incorrect PIN entries.', - unblock: 'Unblock', - unblockCard: 'Unblock card', changePin: 'Change PIN', changePinAtATM: 'Change your PIN at any ATM', changePinAtATMDescription: 'This is required in your region. Reach out to Concierge if you have any questions.', @@ -3082,7 +3072,6 @@ const translations = { mergeBlockScreen: { title: 'Couldn’t add work email', subtitle: (workEmail: string | undefined) => `We couldn’t add ${workEmail}. Please try again later in Settings or chat with Concierge for guidance.`, - workAccountClosedSubtitle: 'The work account associated with this email is closed. Please contact your company admin to reactivate it, or sign up with a different email.', }, tasks: { testDriveAdminTask: { @@ -6448,32 +6437,8 @@ const translations = { }, hr: { title: 'HR', - connections: 'Connections', subtitle: 'Connect HR tools and keep employee approvals in sync.', - connect: 'Connect', - syncNow: 'Sync now', - disconnect: 'Disconnect', - disconnectTitle: (providerName: string) => `Disconnect ${providerName}`, - disconnectPrompt: (providerName: string) => `Are you sure you want to disconnect ${providerName}?`, - lastSync: (relativeDate: string) => `Last synced ${relativeDate}`, - syncError: (providerName: string) => `Can't connect to ${providerName}`, - connectionDescription: (providerName: string) => `Connect ${providerName} to keep employee approvals in sync with your workspace.`, - approvalMode: 'Approval mode', - finalApprover: 'Final approver', - notSet: 'Not set', - approvalModeDescription: (providerName: string) => `Members and managers are set up to sync with ${providerName}.`, - approvalModeWarningTitle: 'Change approval mode?', - approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => - `Are you sure you would like to change the approval mode for this workspace? Learn more about the different ${providerName}-enabled workflow modes in our help site.`, - approvalModeWarningConfirm: 'Change approval mode', - approvalModes: { - basic: {label: 'Basic approval', description: 'All users submit to a single person for processing and approval.'}, - manager: { - label: 'Manager approval', - description: (providerName: string) => `Employees submit reports to their direct manager configured in ${providerName}.`, - }, - custom: {label: 'Custom approval', description: "I'll manually setup approval workflows in Expensify."}, - }, + settingsTitle: 'Gusto settings', syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -6488,8 +6453,6 @@ const translations = { return 'Loading data from TriNet'; case 'zenefitsSyncProvisioning': return 'Provisioning employees in policy'; - case 'mergeHRSyncTitle': - return 'Synchronizing HR Employees'; case 'jobDone': return 'Waiting for imported data to load'; default: { @@ -6499,6 +6462,36 @@ const translations = { }, gusto: { title: 'Gusto', + connect: 'Connect', + syncNow: 'Sync now', + disconnect: 'Disconnect', + lastSync: (relativeDate: string) => `Last synced ${relativeDate}`, + syncError: "Can't connect to Gusto", + disconnectTitle: 'Disconnect Gusto', + disconnectPrompt: 'Are you sure you want to disconnect Gusto?', + connectionDescription: 'Connect Gusto to keep employee approvals in sync with your workspace.', + approvalMode: 'Approval mode', + finalApprover: 'Final approver', + notSet: 'Not set', + approvalModeDescription: 'Members and managers are set up to sync with Gusto.', + approvalModeWarningTitle: 'Change approval mode?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `Are you sure you would like to change the approval mode for this workspace? Learn more about the different Gusto-enabled workflow modes in our help site.`, + approvalModeWarningConfirm: 'Change approval mode', + approvalModes: { + basic: { + label: 'Basic approval', + description: 'All users submit to a single person for processing and approval.', + }, + manager: { + label: 'Manager approval', + description: 'Employees submit reports to their direct manager configured in Gusto.', + }, + custom: { + label: 'Custom approval', + description: 'I’ll manually setup approval workflows in Expensify.', + }, + }, syncResults: { title: 'Gusto sync results', successTitle: 'Successfully synced your Gusto connection!', @@ -6511,8 +6504,42 @@ const translations = { }), }, }, + merge: { + approvalMode: 'Approval mode', + finalApprover: 'Final approver', + }, zenefits: { title: 'TriNet', + connect: 'Connect', + syncNow: 'Sync now', + disconnect: 'Disconnect', + lastSync: (relativeDate: string) => `Last synced ${relativeDate}`, + syncError: "Can't connect to TriNet", + disconnectTitle: 'Disconnect TriNet', + disconnectPrompt: 'Are you sure you want to disconnect TriNet?', + connectionDescription: 'Connect TriNet to keep employee approvals in sync with your workspace.', + approvalMode: 'Approval mode', + finalApprover: 'Final approver', + notSet: 'Not set', + approvalModeDescription: 'Members and managers are set up to sync with TriNet.', + approvalModeWarningTitle: 'Change approval mode?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `Are you sure you would like to change the approval mode for this workspace? Learn more about the different TriNet-enabled workflow modes in our help site.`, + approvalModeWarningConfirm: 'Change approval mode', + approvalModes: { + basic: { + label: 'Basic approval', + description: 'All users submit to a single person for processing and approval.', + }, + manager: { + label: 'Manager approval', + description: 'Employees submit reports to their direct manager configured in TriNet.', + }, + custom: { + label: 'Custom approval', + description: "I'll manually setup approval workflows in Expensify.", + }, + }, }, }, export: { @@ -6711,12 +6738,6 @@ const translations = { onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Advanced approvals are only available on the Control plan, which starts at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, }, - approvalSubmit: { - title: 'Approvals', - description: 'Centrally configure who all members submit to by enabling approvals.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Approvals are available on the Collect and Control plans, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, - }, categories: { title: 'Categories', description: 'Categories allow you to track and organize spend. Use our default categories or add your own.', @@ -6747,12 +6768,6 @@ const translations = { onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `This is only available on the Control plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, }, - companyCardSubmit: { - title: 'Company cards', - description: `Bring your own company card to Expensify to get automatic import, automatic categorization, customizable rule support, and integrated reconciliation.`, - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Company card import is available on the Collect and Control plans, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, - }, rules: { title: 'Rules', description: `Rules run in the background and keep your spend under control so you don't have to sweat the small stuff.\n\nRequire expense details like receipts and descriptions, set limits and defaults, and automate approvals and payments – all in one place.`, @@ -6779,12 +6794,6 @@ const translations = { onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Travel is available on the Collect plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, }, - travelSubmit: { - title: 'Expensify Travel', - description: 'Book discounted flights, hotels, cars, and trains globally from within Expensify, with duty of care reporting and integrated expense management.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Expensify Travel is available on the Collect and Control plans, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, - }, reports: { title: 'Reports', description: 'Reports allow you to group expenses for easier tracking and organization.', @@ -6816,39 +6825,6 @@ const translations = { onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Multiple approval levels are only available on the Control plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, }, - roles: { - title: 'Roles', - description: 'Assign different roles to different members to increase or decrease visibility and control as appropriate.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Roles are available on the Collect and Control plans, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, - }, - payments: { - title: 'Payments', - description: 'Reimburse employees directly from your business bank account.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Payments are available on the Collect and Control plans, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, - }, - accounting: { - title: 'Accounting', - description: - 'Sync categories, tags, tax rates, and more from your accounting system to Expensify, as well as export expense reports and card transactions – no typing (or typos) involved!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Accounting is available on the Collect and Control plans, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, - }, - expensifyCard: { - title: 'Expensify Card', - description: - 'Issue company cards (including virtual cards) directly from your own bank account to get realtime spend control with an unbreakable connection, and up to 2% cashback!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `The Expensify Card is available on the Collect and Control plans, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, - upgradeButton: 'Upgrade and enable', - }, - invoicing: { - title: 'Invoicing', - description: 'Create, send, and track professional invoices, all inside Expensify. Get paid faster with integrated payments and real-time visibility.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Invoicing is available on the Collect and Control plans, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, - }, pricing: { perActiveMember: 'per active member per month.', perMember: 'per member per month.', @@ -7197,8 +7173,8 @@ const translations = { description: 'For employees looking to submit expenses to their employer.', }, }, - description: "Choose a plan that's right for you.", - subscriptionLink: 'Learn more', + description: "Choose a plan that's right for you. For a detailed list of features and pricing, check out our", + subscriptionLink: 'plan types and pricing help page', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `You've committed to 1 active member on the Control plan until your annual subscription ends on ${annualSubscriptionEndDate}. You can switch to pay-per-use subscription and downgrade to the Collect plan starting ${annualSubscriptionEndDate} by disabling auto-renew in`, other: `You've committed to ${count} active members on the Control plan until your annual subscription ends on ${annualSubscriptionEndDate}. You can switch to pay-per-use subscription and downgrade to the Collect plan starting ${annualSubscriptionEndDate} by disabling auto-renew in`, diff --git a/src/languages/es.ts b/src/languages/es.ts index cfcfe238925c..59d55843e76c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -217,7 +217,6 @@ const translations: TranslationDeepObject = { reset: 'Restablecer', done: 'Listo', more: 'Más', - other: 'Otro', debitCard: 'Tarjeta de débito', bankAccount: 'Cuenta bancaria', personalBankAccount: 'Cuenta bancaria personal', @@ -932,12 +931,8 @@ const translations: TranslationDeepObject = { other: (pluralCount: number) => `Tiempo restante: ${pluralCount} días`, }), }, - yourSpend: { - title: 'Tu gasto', - awaitingApproval: 'Pendiente de aprobación', - repaidLast30Days: 'Reembolsado en los últimos 30 días', - recentTransactions: ({lastFour}: {lastFour: string}) => `Transacciones recientes • ${lastFour}`, - }, + assignedCards: 'Tus tarjetas Expensify', + assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} restantes`, announcements: 'Anuncios', discoverSection: { title: 'Descubrir', @@ -2400,11 +2395,6 @@ ${amount} para ${merchant} - ${date}`, pinChanged: '¡PIN cambiado!', pinChangedHeader: 'PIN cambiado', pinChangedDescription: 'Ya puedes usar tu nuevo PIN.', - cardUnblocked: '¡Tarjeta desbloqueada!', - cardUnblockedDescription: 'Es posible que se te pida insertar tu tarjeta en el lector la próxima vez que realices una compra.', - pinBlocked: 'Tu tarjeta fue bloqueada por introducir el PIN incorrectamente varias veces.', - unblock: 'Desbloquear', - unblockCard: 'Desbloquear tarjeta', changePinAtATM: 'Cambia tu PIN en cualquier cajero automático', changePinAtATMDescription: 'Esto es obligatorio en tu región. Contacta a Concierge si tienes alguna pregunta.', freezeCard: 'Congelar tarjeta', @@ -2890,8 +2880,6 @@ ${amount} para ${merchant} - ${date}`, mergeBlockScreen: { title: 'No se pudo añadir el correo electrónico de trabajo', subtitle: (workEmail) => `No pudimos añadir ${workEmail}. Por favor, inténtalo de nuevo más tarde en Configuración o chatea con Concierge para obtener ayuda.`, - workAccountClosedSubtitle: - 'La cuenta de trabajo asociada a este correo electrónico está cerrada. Ponte en contacto con el administrador de tu empresa para reactivarla o regístrate con un correo electrónico diferente.', }, tasks: { testDriveAdminTask: { @@ -6257,32 +6245,8 @@ ${amount} para ${merchant} - ${date}`, }, hr: { title: 'HR', - connections: 'Conexiones', subtitle: 'Conecta herramientas de HR y mantén sincronizadas las aprobaciones de empleados.', - connect: 'Conectar', - syncNow: 'Sincronizar ahora', - disconnect: 'Desconectar', - disconnectTitle: (providerName: string) => `Desconectar ${providerName}`, - disconnectPrompt: (providerName: string) => `¿Seguro que quieres desconectar ${providerName}?`, - lastSync: (relativeDate: string) => `Última sincronización ${relativeDate}`, - syncError: (providerName: string) => `No se puede conectar con ${providerName}`, - connectionDescription: (providerName: string) => `Conecta ${providerName} para mantener sincronizadas las aprobaciones de empleados con tu espacio de trabajo.`, - approvalMode: 'Modo de aprobación', - finalApprover: 'Aprobador final', - notSet: 'No configurado', - approvalModeDescription: (providerName: string) => `Los miembros y gerentes están configurados para sincronizarse con ${providerName}.`, - approvalModeWarningTitle: '¿Cambiar modo de aprobación?', - approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => - `¿Seguro que quieres cambiar el modo de aprobación de este espacio de trabajo? Obtén más información sobre los diferentes modos de flujo de trabajo con ${providerName} en nuestro sitio de ayuda.`, - approvalModeWarningConfirm: 'Cambiar modo de aprobación', - approvalModes: { - basic: {label: 'Aprobación básica', description: 'Todos los usuarios envían a una sola persona para su procesamiento y aprobación.'}, - manager: { - label: 'Aprobación del gerente', - description: (providerName: string) => `Los empleados envían informes a su gerente directo configurado en ${providerName}.`, - }, - custom: {label: 'Aprobación personalizada', description: 'Configuraré manualmente los flujos de aprobación en Expensify.'}, - }, + settingsTitle: 'Configuración de Gusto', syncStageName: ({stage}) => { switch (stage) { case 'gustoSyncTitle': @@ -6297,8 +6261,6 @@ ${amount} para ${merchant} - ${date}`, return 'Cargando datos desde TriNet'; case 'zenefitsSyncProvisioning': return 'Aprovisionar empleados en la política'; - case 'mergeHRSyncTitle': - return 'Sincronización de empleados de HR'; case 'jobDone': return 'Esperando a que se carguen los datos importados'; default: { @@ -6308,6 +6270,36 @@ ${amount} para ${merchant} - ${date}`, }, gusto: { title: 'Gusto', + connect: 'Conectar', + syncNow: 'Sincronizar ahora', + disconnect: 'Desconectar', + lastSync: (relativeDate) => `Última sincronización ${relativeDate}`, + syncError: 'No se puede conectar con Gusto', + disconnectTitle: 'Desconectar Gusto', + disconnectPrompt: '¿Seguro que quieres desconectar Gusto?', + connectionDescription: 'Conecta Gusto para mantener sincronizadas las aprobaciones de empleados con tu espacio de trabajo.', + approvalMode: 'Modo de aprobación', + finalApprover: 'Aprobador final', + notSet: 'No configurado', + approvalModeDescription: 'Los miembros y gerentes están configurados para sincronizarse con Gusto.', + approvalModeWarningTitle: '¿Cambiar modo de aprobación?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `¿Seguro que quieres cambiar el modo de aprobación de este espacio de trabajo? Obtén más información sobre los diferentes modos de flujo de trabajo con Gusto en nuestro sitio de ayuda.`, + approvalModeWarningConfirm: 'Cambiar modo de aprobación', + approvalModes: { + basic: { + label: 'Aprobación básica', + description: 'Todos los usuarios envían a una sola persona para su procesamiento y aprobación.', + }, + manager: { + label: 'Aprobación del gerente', + description: 'Los empleados envían informes a su gerente directo configurado en Gusto.', + }, + custom: { + label: 'Aprobación personalizada', + description: 'Configuraré manualmente los flujos de aprobación en Expensify.', + }, + }, syncResults: { title: 'Resultados de la sincronización de Gusto', successTitle: '¡Se sincronizó correctamente tu conexión con Gusto!', @@ -6320,8 +6312,33 @@ ${amount} para ${merchant} - ${date}`, }), }, }, + merge: { + approvalMode: 'Modo de aprobación', + finalApprover: 'Aprobador final', + }, zenefits: { title: 'TriNet', + connect: 'Conectar', + syncNow: 'Sincronizar ahora', + disconnect: 'Desconectar', + lastSync: (relativeDate: string) => `Última sincronización ${relativeDate}`, + syncError: 'No se puede conectar con TriNet', + disconnectTitle: 'Desconectar TriNet', + disconnectPrompt: '¿Seguro que quieres desconectar TriNet?', + connectionDescription: 'Conecta TriNet para mantener las aprobaciones de empleados sincronizadas con tu espacio de trabajo.', + approvalMode: 'Modo de aprobación', + finalApprover: 'Aprobador final', + notSet: 'No establecido', + approvalModeDescription: 'Los miembros y gestores están configurados para sincronizarse con TriNet.', + approvalModeWarningTitle: '¿Cambiar el modo de aprobación?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `¿Seguro que quieres cambiar el modo de aprobación de este espacio de trabajo? Obtén más información sobre los distintos modos de flujo de trabajo habilitados por TriNet en nuestro sitio de ayuda.`, + approvalModeWarningConfirm: 'Cambiar el modo de aprobación', + approvalModes: { + basic: {label: 'Aprobación básica', description: 'Todos los usuarios envían a una sola persona para su procesamiento y aprobación.'}, + manager: {label: 'Aprobación del responsable', description: 'Los empleados envían informes a su responsable directo configurado en TriNet.'}, + custom: {label: 'Aprobación personalizada', description: 'Configuraré manualmente los flujos de aprobación en Expensify.'}, + }, }, }, export: { @@ -6506,8 +6523,8 @@ ${amount} para ${merchant} - ${date}`, description: 'Para empleados que buscan enviar gastos a su empleador.', }, }, - description: 'Elige el plan adecuado para ti.', - subscriptionLink: 'Más información', + description: 'Elige el plan adecuado para ti. Para ver una lista detallada de funciones y precios, consulta nuestra', + subscriptionLink: 'página de ayuda sobre tipos de planes y precios', lockedPlanDescription: ({count, annualSubscriptionEndDate}) => ({ one: `Tienes un compromiso anual de 1 miembro activo en el plan Controlar hasta el ${annualSubscriptionEndDate}. Puedes cambiar a una suscripción de pago por uso y desmejorar al plan Recopilar a partir del ${annualSubscriptionEndDate} desactivando la renovación automática en`, other: `Tienes un compromiso anual de ${count} miembros activos en el plan Controlar hasta el ${annualSubscriptionEndDate}. Puedes cambiar a una suscripción de pago por uso y desmejorar al plan Recopilar a partir del ${annualSubscriptionEndDate} desactivando la renovación automática en`, @@ -6545,12 +6562,6 @@ ${amount} para ${merchant} - ${date}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => `Las aprobaciones avanzadas sólo están disponibles en el plan Controlar, con precios desde ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, }, - approvalSubmit: { - title: 'Aprobaciones', - description: 'Configura de forma centralizada a quién envían todos los miembros sus gastos al activar las aprobaciones.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => - `Las aprobaciones están disponibles en los planes Recopilar y Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, - }, categories: { title: 'Categorías', description: 'Las categorías te permiten rastrear y organizar gastos. Usa nuestras categorías predeterminadas o añade las tuyas propias.', @@ -6581,12 +6592,6 @@ ${amount} para ${merchant} - ${date}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => `Esto solo está disponible en el plan Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, }, - companyCardSubmit: { - title: 'Tarjetas de empresa', - description: 'Trae tu propia tarjeta de empresa a Expensify para obtener importación automática, categorización automática, reglas personalizables y conciliación integrada.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => - `La importación de tarjetas de empresa está disponible en los planes Recopilar y Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, - }, rules: { title: 'Reglas', description: `Las reglas se ejecutan en segundo plano y mantienen tus gastos bajo control para que no tengas que preocuparte por los detalles pequeños.\n\nExige detalles de los gastos, como recibos y descripciones, establece límites y valores predeterminados, y automatiza las aprobaciones y los pagos, todo en un mismo lugar.`, @@ -6614,12 +6619,6 @@ ${amount} para ${merchant} - ${date}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => `Los viajes están disponibles en el plan Recopilar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, }, - travelSubmit: { - title: 'Expensify Travel', - description: 'Reserva vuelos, hoteles, coches y trenes con descuento en todo el mundo desde Expensify, con informes de duty of care y gestión de gastos integrada.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => - `Expensify Travel está disponible en los planes Recopilar y Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, - }, reports: { title: 'Informes', description: 'Los informes te permiten agrupar gastos para un seguimiento y organización más fáciles.', @@ -6652,39 +6651,6 @@ ${amount} para ${merchant} - ${date}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => `Los múltiples niveles de aprobación solo están disponibles en el plan Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, }, - roles: { - title: 'Roles', - description: 'Asigna diferentes roles a diferentes miembros para aumentar o disminuir la visibilidad y el control según corresponda.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => - `Los roles están disponibles en los planes Recopilar y Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, - }, - payments: { - title: 'Pagos', - description: 'Reembolsa a los empleados directamente desde la cuenta bancaria de tu empresa.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => - `Los pagos están disponibles en los planes Recopilar y Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, - }, - accounting: { - title: 'Contabilidad', - description: - 'Sincroniza categorías, etiquetas, tasas de impuestos y más desde tu sistema contable a Expensify, además de exportar informes de gastos y transacciones de tarjetas, ¡sin necesidad de escribir (ni errores tipográficos)!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => - `La contabilidad está disponible en los planes Recopilar y Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, - }, - expensifyCard: { - title: 'Tarjeta Expensify', - description: - 'Emite tarjetas de empresa (incluidas tarjetas virtuales) directamente desde tu propia cuenta bancaria para obtener control de gastos en tiempo real con una conexión irrompible, ¡y hasta un 2% de reembolso!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => - `La Tarjeta Expensify está disponible en los planes Recopilar y Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, - upgradeButton: 'Actualizar y activar', - }, - invoicing: { - title: 'Facturación', - description: 'Crea, envía y rastrea facturas profesionales, todo dentro de Expensify. Recibe pagos más rápido con pagos integrados y visibilidad en tiempo real.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => - `La facturación está disponible en los planes Recopilar y Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, - }, note: (subscriptionLink) => `Mejore para acceder a esta función, o más información sobre nuestros planes y precios.`, pricing: { perActiveMember: 'por miembro activo al mes.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index f217296b269d..4ec6dad932ff 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -261,7 +261,6 @@ const translations: TranslationDeepObject = { reset: 'Réinitialiser', done: 'Terminé', more: 'Plus', - other: 'Autre', debitCard: 'Carte de débit', bankAccount: 'Compte bancaire', personalBankAccount: 'Compte bancaire personnel', @@ -961,6 +960,8 @@ const translations: TranslationDeepObject = { personalSubtitle: 'Portefeuille', }, }, + assignedCards: 'Vos cartes Expensify', + assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} restant`, announcements: 'Annonces', discoverSection: { title: 'Découvrir', @@ -1040,12 +1041,6 @@ const translations: TranslationDeepObject = { setupRules: 'Configurer les règles de dépense', inviteAccountant: 'Inviter votre comptable', }, - yourSpend: { - title: 'Vos dépenses', - awaitingApproval: 'En attente d’approbation', - repaidLast30Days: 'Remboursé au cours des 30 derniers jours', - recentTransactions: ({lastFour}: {lastFour: string}) => `Transactions récentes • ${lastFour}`, - }, }, allSettingsScreen: { subscription: 'Abonnement', @@ -2551,11 +2546,6 @@ ${amount} pour ${merchant} - ${date}`, frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Cette carte a été gelée par ${person}. Veuillez contacter un administrateur pour la dégeler.`, spendRules: 'Règles de dépense', editSpendRules: 'Modifier les règles de dépenses', - cardUnblocked: 'Carte débloquée !', - cardUnblockedDescription: 'Il se peut que vous soyez invité à insérer votre carte dans le lecteur la prochaine fois que vous effectuerez un achat.', - pinBlocked: 'Votre carte a été bloquée en raison de saisies de code PIN incorrectes.', - unblock: 'Débloquer', - unblockCard: 'Débloquer la carte', }, workflowsPage: { workflowTitle: 'Dépense', @@ -3026,8 +3016,6 @@ ${amount} pour ${merchant} - ${date}`, title: 'Impossible d’ajouter l’adresse e-mail professionnelle', subtitle: (workEmail: string | undefined) => `Nous n’avons pas pu ajouter ${workEmail}. Veuillez réessayer plus tard dans les Paramètres ou discuter avec Concierge pour obtenir de l’aide.`, - workAccountClosedSubtitle: - 'Le compte professionnel associé à cet e-mail est fermé. Veuillez contacter l’administrateur de votre entreprise pour le réactiver, ou inscrivez-vous avec un autre e-mail.', }, tasks: { testDriveAdminTask: { @@ -6776,59 +6764,6 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Les intégrations RH sont uniquement disponibles avec l’offre Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, }, - approvalSubmit: { - title: 'Approbations', - description: 'Configurez de manière centralisée à qui tous les membres soumettent en activant les approbations.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Les validations sont disponibles avec les forfaits Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, - }, - companyCardSubmit: { - title: 'Cartes d’entreprise', - description: `Apportez votre propre carte professionnelle dans Expensify pour bénéficier de l’importation automatique, de la catégorisation automatique, de règles personnalisables et du rapprochement intégré.`, - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `L’importation de cartes d’entreprise est disponible avec les offres Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, - }, - travelSubmit: { - title: 'Expensify Travel', - description: - 'Réservez des vols, hôtels, voitures et trains à tarif réduit partout dans le monde depuis Expensify, avec rapports de devoir de diligence et gestion intégrée des dépenses.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Expensify Travel est disponible avec les forfaits Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, - }, - roles: { - title: 'Rôles', - description: 'Attribuez différents rôles aux différents membres pour augmenter ou réduire la visibilité et le contrôle selon les besoins.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Les rôles sont disponibles avec les formules Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, - }, - payments: { - title: 'Paiements', - description: 'Remboursez vos employé·e·s directement depuis le compte bancaire de votre entreprise.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Les paiements sont disponibles avec les offres Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, - }, - accounting: { - title: 'Comptabilité', - description: - 'Synchronisez les catégories, les tags, les taux de taxe et plus encore depuis votre système comptable vers Expensify, et exportez également les notes de frais et les transactions par carte – sans saisie (ni fautes de frappe) !', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `La comptabilité est disponible avec les offres Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, - }, - expensifyCard: { - title: 'Carte Expensify', - description: - 'Émettez des cartes d’entreprise (y compris des cartes virtuelles) directement depuis votre propre compte bancaire pour bénéficier d’un contrôle des dépenses en temps réel avec une connexion infaillible, et jusqu’à 2 % de cashback !', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `La carte Expensify est disponible avec les offres Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, - upgradeButton: 'Mettre à niveau et activer', - }, - invoicing: { - title: 'Facturation', - description: - 'Créez, envoyez et suivez des factures professionnelles, le tout dans Expensify. Soyez payé plus rapidement grâce aux paiements intégrés et à la visibilité en temps réel.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `La facturation est disponible avec les offres Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, - }, }, downgrade: { commonFeatures: { @@ -7155,8 +7090,8 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e description: 'Pour les employés souhaitant soumettre des dépenses à leur employeur.', }, }, - description: 'Choisissez l’offre qui vous convient.', - subscriptionLink: 'En savoir plus', + description: 'Choisissez l’offre qui vous convient. Pour une liste détaillée des fonctionnalités et des tarifs, consultez notre', + subscriptionLink: "page d'aide sur les types de forfaits et les tarifs", lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `Vous vous êtes engagé à 1 membre actif sur le plan Control jusqu'à la fin de votre abonnement annuel, le ${annualSubscriptionEndDate}. Vous pourrez passer à un abonnement à l’usage et rétrograder vers le plan Collect à partir du ${annualSubscriptionEndDate} en désactivant le renouvellement automatique dans`, other: `Vous vous êtes engagé·e à avoir ${count} membres actifs sur le forfait Control jusqu’à la fin de votre abonnement annuel le ${annualSubscriptionEndDate}. Vous pouvez passer à un abonnement à l’usage et rétrograder vers le forfait Collect à partir du ${annualSubscriptionEndDate} en désactivant le renouvellement automatique dans`, @@ -7165,38 +7100,8 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e }, hr: { title: 'RH', - connections: 'Connexions', subtitle: 'Connectez vos outils RH et gardez les approbations des employés synchronisées.', - connect: 'Connecter', - syncNow: 'Synchroniser maintenant', - disconnect: 'Déconnecter', - disconnectTitle: (providerName: string) => `Déconnecter ${providerName}`, - disconnectPrompt: (providerName: string) => `Êtes-vous sûr de vouloir déconnecter ${providerName} ?`, - lastSync: (relativeDate: string) => `Dernière synchronisation ${relativeDate}`, - syncError: (providerName: string) => `Impossible de se connecter à ${providerName}`, - connectionDescription: (providerName: string) => `Connectez ${providerName} pour synchroniser les approbations des employés avec votre espace de travail.`, - approvalMode: "Mode d'approbation", - finalApprover: 'Approbateur final', - notSet: 'Non défini', - approvalModeDescription: (providerName: string) => `Les membres et les responsables sont configurés pour se synchroniser avec ${providerName}.`, - approvalModeWarningTitle: 'Changer le mode d’approbation ?', - approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => - `Êtes-vous sûr·e de vouloir modifier le mode d’approbation de cet espace de travail ? En savoir plus sur les différents modes de workflow compatibles avec ${providerName} sur notre site d’aide.`, - approvalModeWarningConfirm: 'Modifier le mode d’approbation', - approvalModes: { - basic: { - label: 'Approbation simple', - description: 'Tous les utilisateurs soumettent à une seule personne pour traitement et approbation.', - }, - manager: { - label: 'Approbation du responsable', - description: (providerName: string) => `Les employé·e·s soumettent leurs rapports à leur responsable direct configuré dans ${providerName}.`, - }, - custom: { - label: 'Approbation personnalisée', - description: 'Je configurerai manuellement les circuits de validation dans Expensify.', - }, - }, + settingsTitle: 'Paramètres Gusto', syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -7220,6 +7125,27 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e }, gusto: { title: 'Gusto', + approvalMode: 'Mode d’approbation', + finalApprover: 'Approbateur final', + notSet: 'Non défini', + approvalModeDescription: 'Les membres et les responsables sont configurés pour se synchroniser avec Gusto.', + approvalModeWarningTitle: 'Changer le mode d’approbation ?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `Êtes-vous sûr·e de vouloir modifier le mode d’approbation de cet espace de travail ? En savoir plus sur les différents modes de workflow compatibles avec Gusto sur notre site d’aide.`, + approvalModeWarningConfirm: 'Modifier le mode d’approbation', + approvalModes: { + basic: {label: 'Approbation simple', description: 'Tous les utilisateurs soumettent à une seule personne pour traitement et approbation.'}, + manager: {label: 'Approbation du responsable', description: 'Les employé·e·s soumettent leurs rapports à leur responsable direct configuré dans Gusto.'}, + custom: {label: 'Approbation personnalisée', description: 'Je configurerai manuellement les circuits de validation dans Expensify.'}, + }, + connect: 'Connect', + connectionDescription: 'Connectez Gusto pour synchroniser les validations des employé·e·s avec votre espace de travail.', + syncNow: 'Synchroniser maintenant', + disconnect: 'Déconnecter', + lastSync: (relativeDate: string) => `Dernière synchronisation ${relativeDate}`, + syncError: 'Impossible de se connecter à Gusto', + disconnectTitle: 'Déconnecter Gusto', + disconnectPrompt: 'Voulez-vous vraiment déconnecter Gusto ?', syncResults: { title: 'Résultats de la synchronisation Gusto', successTitle: 'Connexion Gusto synchronisée avec succès !', @@ -7232,8 +7158,33 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e }), }, }, + merge: { + approvalMode: 'Mode d’approbation', + finalApprover: 'Approbateur final', + }, zenefits: { title: 'TriNet', + connect: 'Connecter', + syncNow: 'Synchroniser maintenant', + disconnect: 'Déconnecter', + lastSync: (relativeDate: string) => `Dernière synchronisation ${relativeDate}`, + syncError: 'Impossible de se connecter à TriNet', + disconnectTitle: 'Déconnecter TriNet', + disconnectPrompt: 'Voulez-vous vraiment déconnecter TriNet ?', + connectionDescription: 'Connectez TriNet pour synchroniser les approbations des employés avec votre espace de travail.', + approvalMode: 'Mode d’approbation', + finalApprover: 'Approbateur final', + notSet: 'Non défini', + approvalModeDescription: 'Les membres et les responsables sont configurés pour se synchroniser avec TriNet.', + approvalModeWarningTitle: 'Changer le mode d’approbation ?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `Voulez-vous vraiment modifier le mode d’approbation de cet espace de travail ? En savoir plus sur les différents modes de workflow compatibles TriNet sur notre site d’aide.`, + approvalModeWarningConfirm: 'Modifier le mode d’approbation', + approvalModes: { + basic: {label: 'Approbation de base', description: 'Tous les utilisateurs soumettent à une seule personne pour traitement et approbation.'}, + manager: {label: 'Approbation du responsable', description: 'Les employés soumettent des notes de frais à leur supérieur hiérarchique direct configuré dans TriNet.'}, + custom: {label: 'Approbation personnalisée', description: 'Je configurerai manuellement les workflows d’approbation dans Expensify.'}, + }, }, }, }, diff --git a/src/languages/it.ts b/src/languages/it.ts index 04714349e740..7caa746dda7c 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -261,7 +261,6 @@ const translations: TranslationDeepObject = { reset: 'Reimposta', done: 'Completato', more: 'Altro', - other: 'Altro', debitCard: 'Carta di debito', bankAccount: 'Conto bancario', personalBankAccount: 'Conto bancario personale', @@ -959,6 +958,8 @@ const translations: TranslationDeepObject = { personalSubtitle: 'Portafoglio', }, }, + assignedCards: 'Le tue Carte Expensify', + assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} rimanenti`, announcements: 'Annunci', discoverSection: { title: 'Scopri', @@ -1038,12 +1039,6 @@ const translations: TranslationDeepObject = { setupRules: 'Configura le regole di spesa', inviteAccountant: 'Invita il tuo commercialista', }, - yourSpend: { - title: 'Le tue spese', - awaitingApproval: 'In attesa di approvazione', - repaidLast30Days: 'Rimborsato negli ultimi 30 giorni', - recentTransactions: ({lastFour}: {lastFour: string}) => `Transazioni recenti • ${lastFour}`, - }, }, allSettingsScreen: { subscription: 'Abbonamento', @@ -2541,11 +2536,6 @@ ${amount} per ${merchant} - ${date}`, frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Questa carta è stata bloccata da ${person}. Contatta un amministratore per sbloccarla.`, spendRules: 'Regole di spesa', editSpendRules: 'Modifica regole di spesa', - cardUnblocked: 'Carta sbloccata!', - cardUnblockedDescription: 'Potrebbe esserti richiesto di inserire la carta nel lettore la prossima volta che effettui un acquisto.', - pinBlocked: 'La tua carta è stata bloccata a causa di PIN inseriti in modo errato.', - unblock: 'Sblocca', - unblockCard: 'Sblocca carta', }, workflowsPage: { workflowTitle: 'Spesa', @@ -3014,8 +3004,6 @@ ${amount} per ${merchant} - ${date}`, title: "Impossibile aggiungere l'email di lavoro", subtitle: (workEmail: string | undefined) => `Non è stato possibile aggiungere ${workEmail}. Riprova più tardi in Impostazioni oppure chatta con Concierge per ricevere assistenza.`, - workAccountClosedSubtitle: - 'L’account di lavoro associato a questa email è chiuso. Contatta l’amministratore della tua azienda per riattivarlo oppure registrati con un’altra email.', }, tasks: { testDriveAdminTask: { @@ -6739,59 +6727,6 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Le integrazioni HR sono disponibili solo con il piano Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, }, - approvalSubmit: { - title: 'Approvazioni', - description: 'Configura centralmente a chi tutti i membri inviano le note spese abilitando le approvazioni.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Le approvazioni sono disponibili nei piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, - }, - companyCardSubmit: { - title: 'Carte aziendali', - description: `Porta la tua carta aziendale in Expensify per ottenere importazione automatica, categorizzazione automatica, supporto per regole personalizzabili e riconciliazione integrata.`, - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `L'importazione della carta aziendale è disponibile con i piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, - }, - travelSubmit: { - title: 'Expensify Travel', - description: - 'Prenota in tutto il mondo voli, hotel, auto e treni scontati direttamente da Expensify, con reportistica per il duty of care e gestione integrata delle note spese.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Expensify Travel è disponibile con i piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, - }, - roles: { - title: 'Ruoli', - description: 'Assegna ruoli diversi ai vari membri per aumentare o ridurre visibilità e controllo secondo le esigenze.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `I ruoli sono disponibili nei piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, - }, - payments: { - title: 'Pagamenti', - description: 'Rimborsa i dipendenti direttamente dal conto bancario aziendale.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `I pagamenti sono disponibili nei piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, - }, - accounting: { - title: 'Contabilità', - description: - 'Sincronizza categorie, tag, aliquote fiscali e altro dal tuo sistema contabile a Expensify ed esporta anche note spese e transazioni con carta, senza bisogno di digitare nulla (né di rischiare errori di battitura)!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `La contabilità è disponibile con i piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, - }, - expensifyCard: { - title: 'Carta Expensify', - description: - 'Emetti carte aziendali (incluse carte virtuali) direttamente dal tuo conto bancario per ottenere il controllo in tempo reale delle spese con una connessione infallibile e fino al 2% di cashback!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `La Expensify Card è disponibile con i piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, - upgradeButton: 'Aggiorna e abilita', - }, - invoicing: { - title: 'Fatturazione', - description: - 'Crea, invia e monitora fatture professionali, tutto all’interno di Expensify. Ricevi i pagamenti più velocemente con pagamenti integrati e visibilità in tempo reale.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `La fatturazione è disponibile nei piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`}`, - }, }, downgrade: { commonFeatures: { @@ -7119,8 +7054,8 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, description: 'Per i dipendenti che desiderano inviare le spese al proprio datore di lavoro.', }, }, - description: 'Scegli il piano più adatto a te.', - subscriptionLink: 'Scopri di più', + description: 'Scegli il piano più adatto a te. Per un elenco dettagliato di funzionalità e prezzi, consulta la nostra', + subscriptionLink: 'pagina di aiuto su tipi di piano e prezzi', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `Ti sei impegnato per 1 membro attivo nel piano Control fino al termine dell’abbonamento annuale, il ${annualSubscriptionEndDate}. Puoi passare all’abbonamento a consumo e effettuare il downgrade al piano Collect a partire dal ${annualSubscriptionEndDate} disattivando il rinnovo automatico in`, other: `Ti sei impegnato per ${count} membri attivi nel piano Control fino alla fine dell’abbonamento annuale, il ${annualSubscriptionEndDate}. Puoi passare all’abbonamento a consumo e effettuare il downgrade al piano Collect a partire dal ${annualSubscriptionEndDate} disattivando il rinnovo automatico in`, @@ -7129,32 +7064,8 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, }, hr: { title: 'Risorse umane', - connections: 'Connessioni', subtitle: 'Collega gli strumenti HR e mantieni sincronizzate le approvazioni dei dipendenti.', - connect: 'Connetti', - syncNow: 'Sincronizza ora', - disconnect: 'Disconnetti', - disconnectTitle: (providerName: string) => `Disconnetti ${providerName}`, - disconnectPrompt: (providerName: string) => `Sei sicuro di voler disconnettere ${providerName}?`, - lastSync: (relativeDate: string) => `Ultima sincronizzazione ${relativeDate}`, - syncError: (providerName: string) => `Impossibile connettersi a ${providerName}`, - connectionDescription: (providerName: string) => `Collega ${providerName} per mantenere sincronizzate le approvazioni dei dipendenti con il tuo spazio di lavoro.`, - approvalMode: 'Modalità di approvazione', - finalApprover: 'Approvatore finale', - notSet: 'Non impostato', - approvalModeDescription: (providerName: string) => `I membri e i responsabili sono configurati per la sincronizzazione con ${providerName}.`, - approvalModeWarningTitle: 'Cambiare modalità di approvazione?', - approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => - `Sei sicuro di voler cambiare la modalità di approvazione per questo spazio di lavoro? Scopri di più sulle diverse modalità di flusso di lavoro abilitate per ${providerName} nel nostro sito di assistenza.`, - approvalModeWarningConfirm: 'Cambia modalità di approvazione', - approvalModes: { - basic: {label: 'Approvazione di base', description: 'Tutti gli utenti inviano a una singola persona per l’elaborazione e l’approvazione.'}, - manager: { - label: 'Approvazione del responsabile', - description: (providerName: string) => `I dipendenti inviano i report al proprio responsabile diretto configurato in ${providerName}.`, - }, - custom: {label: 'Approvazione personalizzata', description: 'Imposterò manualmente i flussi di approvazione in Expensify.'}, - }, + settingsTitle: 'Impostazioni Gusto', syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -7178,6 +7089,27 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, }, gusto: { title: 'Gusto', + approvalMode: 'Modalità approvazione', + finalApprover: 'Approvazione finale', + notSet: 'Non impostato', + approvalModeDescription: 'I membri e i responsabili sono configurati per la sincronizzazione con Gusto.', + approvalModeWarningTitle: 'Cambiare modalità di approvazione?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `Sei sicuro di voler cambiare la modalità di approvazione per questo spazio di lavoro? Scopri di più sulle diverse modalità di flusso di lavoro abilitate per Gusto nel nostro sito di assistenza.`, + approvalModeWarningConfirm: 'Cambia modalità di approvazione', + approvalModes: { + basic: {label: 'Approvazione di base', description: 'Tutti gli utenti inviano a una singola persona per l’elaborazione e l’approvazione.'}, + manager: {label: 'Approvazione del responsabile', description: 'I dipendenti inviano i report al proprio responsabile diretto configurato in Gusto.'}, + custom: {label: 'Approvazione personalizzata', description: 'Imposterò manualmente i flussi di approvazione in Expensify.'}, + }, + connect: 'Collega', + connectionDescription: 'Collega Gusto per sincronizzare le approvazioni dei dipendenti con il tuo spazio di lavoro.', + syncNow: 'Sincronizza ora', + disconnect: 'Disconnetti', + lastSync: (relativeDate: string) => `Ultima sincronizzazione ${relativeDate}`, + syncError: 'Impossibile connettersi a Gusto', + disconnectTitle: 'Disconnetti Gusto', + disconnectPrompt: 'Sei sicuro di voler disconnettere Gusto?', syncResults: { title: 'Risultati sincronizzazione Gusto', successTitle: 'Connessione a Gusto sincronizzata con successo!', @@ -7190,8 +7122,33 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, }), }, }, + merge: { + approvalMode: 'Modalità approvazione', + finalApprover: 'Approvazione finale', + }, zenefits: { title: 'TriNet', + connect: 'Connetti', + syncNow: 'Sincronizza ora', + disconnect: 'Disconnetti', + lastSync: (relativeDate: string) => `Ultima sincronizzazione ${relativeDate}`, + syncError: 'Impossibile connettersi a TriNet', + disconnectTitle: 'Disconnetti TriNet', + disconnectPrompt: 'Sei sicuro di voler disconnettere TriNet?', + connectionDescription: 'Collega TriNet per mantenere sincronizzate le approvazioni dei dipendenti con il tuo spazio di lavoro.', + approvalMode: 'Modalità approvazione', + finalApprover: 'Responsabile finale approvazione', + notSet: 'Non impostato', + approvalModeDescription: 'Membri e responsabili sono configurati per sincronizzarsi con TriNet.', + approvalModeWarningTitle: 'Cambiare modalità di approvazione?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `Sei sicuro di voler cambiare la modalità di approvazione per questo workspace? Scopri di più sulle diverse modalità di workflow abilitate per TriNet nel nostro sito di assistenza.`, + approvalModeWarningConfirm: 'Modifica modalità di approvazione', + approvalModes: { + basic: {label: 'Approvazione di base', description: 'Tutti gli utenti inviano a una sola persona per l’elaborazione e l’approvazione.'}, + manager: {label: 'Approvazione del manager', description: 'I dipendenti inviano i report al loro responsabile diretto configurato in TriNet.'}, + custom: {label: 'Approvazione personalizzata', description: 'Imposterò manualmente i flussi di approvazione in Expensify.'}, + }, }, }, }, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c6ee83df7138..c191fbf38b2f 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -261,7 +261,6 @@ const translations: TranslationDeepObject = { reset: 'リセット', done: '完了', more: 'その他', - other: 'その他', debitCard: 'デビットカード', bankAccount: '銀行口座', personalBankAccount: '個人銀行口座', @@ -942,6 +941,8 @@ const translations: TranslationDeepObject = { personalSubtitle: 'ウォレット', }, }, + assignedCards: 'お客様の Expensify カード', + assignedCardsRemaining: ({amount}: {amount: string}) => `残額:${amount}`, announcements: 'お知らせ', discoverSection: { title: '発見', @@ -1021,12 +1022,6 @@ const translations: TranslationDeepObject = { setupRules: '支出ルールを設定', inviteAccountant: '会計士を招待', }, - yourSpend: { - title: 'あなたの支出', - awaitingApproval: '承認待ち', - repaidLast30Days: '過去30日間に返済済み', - recentTransactions: ({lastFour}: {lastFour: string}) => `最近の取引 • ${lastFour}`, - }, }, allSettingsScreen: { subscription: 'サブスクリプション', @@ -2516,11 +2511,6 @@ ${date} の ${merchant} への ${amount}`, frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `このカードは${person}によって一時停止されました。解除するには管理者に連絡してください。`, spendRules: '支出ルール', editSpendRules: '支出ルールを編集', - cardUnblocked: 'カードのブロックを解除しました!', - cardUnblockedDescription: '次回お買い物をされる際に、カードをリーダーに挿入するよう求められる場合があります。', - pinBlocked: '暗証番号の誤入力が続いたため、カードをブロックしました。', - unblock: 'ブロック解除', - unblockCard: 'カードのブロックを解除', }, workflowsPage: { workflowTitle: '支出', @@ -2987,8 +2977,6 @@ ${date} の ${merchant} への ${amount}`, title: '勤務用メールアドレスを追加できませんでした', subtitle: (workEmail: string | undefined) => `${workEmail} を追加できませんでした。後で「設定」からもう一度お試しいただくか、ガイダンスについて Concierge にチャットでお問い合わせください。`, - workAccountClosedSubtitle: - 'このメールアドレスに関連付けられている業務用アカウントは停止されています。再有効化するには会社の管理者にご連絡いただくか、別のメールアドレスでサインアップしてください。', }, tasks: { testDriveAdminTask: { @@ -6665,56 +6653,6 @@ ${reportName} onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `人事システム連携は、${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり、1か月ごと。`}からのControlプランでのみご利用いただけます`, }, - approvalSubmit: { - title: '承認', - description: '承認機能を有効にして、全メンバーの提出先を一括設定しましょう。', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `承認機能は、Collect プランおよび Control プランで利用できます。料金は ${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からです。`, - }, - companyCardSubmit: { - title: '会社カード', - description: `お使いの会社カードをExpensifyに連携して、自動取込、自動分類、カスタマイズ可能なルール設定、そして統合された照合機能を利用しましょう。`, - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `会社カードのインポートは、Collect プランと Control プランで利用できます。料金は ${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からです。`, - }, - travelSubmit: { - title: 'Expensify トラベル', - description: 'Expensify から世界中の割引航空券、ホテル、レンタカー、列車を予約し、デューティ・オブ・ケアレポートと経費管理を統合して利用できます。', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Expensify Travel は Collect プランと Control プランで利用でき、${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からご利用いただけます`, - }, - roles: { - title: 'ロール', - description: 'メンバーごとに異なるロールを割り当て、必要に応じて閲覧権限や管理権限を増減させましょう。', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `ロールは、Collect および Control プランで利用できます。${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からご利用いただけます`, - }, - payments: { - title: '支払い', - description: '従業員への精算を、事業用銀行口座から直接行います。', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `支払い機能は、Collect プランおよび Control プランで利用できます。料金は ${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からです`, - }, - accounting: { - title: '会計', - description: '会計システムからExpensifyへカテゴリ、タグ、税率などを同期し、経費レポートやカード取引をエクスポートできます。手入力も、入力ミスの心配も不要です!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `会計機能は Collect プランと Control プランでご利用いただけます。${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`}からご利用可能です`, - }, - expensifyCard: { - title: 'Expensify カード', - description: - '自社の銀行口座から(バーチャルカードを含む)コーポレートカードを直接発行し、途切れない連携によるリアルタイムな支出管理と、最大2%のキャッシュバックを手に入れましょう!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Expensify Card は Collect プランと Control プランでご利用いただけます。料金は ${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からです。`, - upgradeButton: 'アップグレードして有効化', - }, - invoicing: { - title: '請求書発行', - description: 'Expensify 内でプロ仕様の請求書を作成・送信・追跡。統合された支払い機能とリアルタイムの可視性で、より早く入金を受けましょう。', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `請求書発行機能は、Collect プランと Control プランでご利用いただけます。${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`}からご利用可能です。`, - }, }, downgrade: { commonFeatures: { @@ -7038,8 +6976,8 @@ ${reportName} description: '雇用主に経費を提出したい従業員向け。', }, }, - description: '自分に合ったプランをお選びください。', - subscriptionLink: '詳しく見る', + description: '自分に合ったプランをお選びください。機能と料金の詳細な一覧は、こちらのページをご覧ください', + subscriptionLink: 'プランの種類と料金のヘルプページ', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `${annualSubscriptionEndDate} までの年間サブスクリプション期間中、Control プランでアクティブメンバー 1 名を利用することに同意しています。${annualSubscriptionEndDate} 以降、 自動更新を無効にすることで、従量課金サブスクリプションに切り替え、Collect プランへダウングレードできます。`, other: `あなたは、年間サブスクリプションが${annualSubscriptionEndDate}に終了するまで、Controlプランでアクティブメンバー${count}名を契約しています。${annualSubscriptionEndDate}以降は、自動更新を無効にすることで、従量課金制サブスクリプションに切り替え、Collectプランへダウングレードできます。その操作は、`, @@ -7048,32 +6986,8 @@ ${reportName} }, hr: { title: '人事', - connections: '接続', subtitle: '人事ツールを連携して、従業員の承認を常に同期させます。', - connect: '接続', - syncNow: '今すぐ同期', - disconnect: '切断', - disconnectTitle: (providerName: string) => `${providerName}を切断`, - disconnectPrompt: (providerName: string) => `${providerName}を切断してもよろしいですか?`, - lastSync: (relativeDate: string) => `最終同期: ${relativeDate}`, - syncError: (providerName: string) => `${providerName}に接続できません`, - connectionDescription: (providerName: string) => `${providerName}を接続して、従業員の承認をワークスペースと同期させましょう。`, - approvalMode: '承認モード', - finalApprover: '最終承認者', - notSet: '未設定', - approvalModeDescription: (providerName: string) => `メンバーとマネージャーは ${providerName} と同期するように設定されています。`, - approvalModeWarningTitle: '承認モードを変更しますか?', - approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => - `このワークスペースの承認モードを変更してもよろしいですか?${providerName} 対応の各ワークフローモードについては、ヘルプサイトで詳しくご覧いただけます。`, - approvalModeWarningConfirm: '承認モードを変更', - approvalModes: { - basic: {label: '基本承認', description: 'すべてのユーザーは、処理と承認のために 1 人の担当者に提出します。'}, - manager: { - label: 'マネージャー承認', - description: (providerName: string) => `従業員は、${providerName} で設定された直属のマネージャーにレポートを提出します。`, - }, - custom: {label: 'カスタム承認', description: 'Expensify で承認ワークフローを手動で設定します。'}, - }, + settingsTitle: 'Gusto 設定', syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -7097,6 +7011,27 @@ ${reportName} }, gusto: { title: 'Gusto', + approvalMode: '承認モード', + finalApprover: '最終承認者', + notSet: '未設定', + approvalModeDescription: 'メンバーとマネージャーは Gusto と同期するように設定されています。', + approvalModeWarningTitle: '承認モードを変更しますか?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `このワークスペースの承認モードを変更してもよろしいですか?Gusto 対応の各ワークフローモードについては、ヘルプサイトで詳しくご覧いただけます。`, + approvalModeWarningConfirm: '承認モードを変更', + approvalModes: { + basic: {label: '基本承認', description: 'すべてのユーザーは、処理と承認のために 1 人の担当者に提出します。'}, + manager: {label: 'マネージャー承認', description: '従業員は、Gusto で設定された直属のマネージャーにレポートを提出します。'}, + custom: {label: 'カスタム承認', description: 'Expensify で承認ワークフローを手動で設定します。'}, + }, + connect: '接続', + connectionDescription: 'Gusto を接続して、従業員の承認をワークスペースと同期させましょう。', + syncNow: '今すぐ同期', + disconnect: '切断', + lastSync: (relativeDate: string) => `最終同期:${relativeDate}`, + syncError: 'Gusto に接続できません', + disconnectTitle: 'Gusto の接続を解除', + disconnectPrompt: 'Gusto との接続を本当に解除しますか?', syncResults: { title: 'Gusto 同期結果', successTitle: 'Gusto との連携が正常に同期されました!', @@ -7109,8 +7044,33 @@ ${reportName} }), }, }, + merge: { + approvalMode: '承認モード', + finalApprover: '最終承認者', + }, zenefits: { title: 'TriNet', + connect: '接続', + syncNow: '今すぐ同期', + disconnect: '切断する', + lastSync: (relativeDate: string) => `最終同期日時:${relativeDate}`, + syncError: 'TriNet に接続できません', + disconnectTitle: 'TriNet の接続を解除', + disconnectPrompt: 'TriNet との接続を解除してもよろしいですか?', + connectionDescription: 'TriNet を接続して、従業員の承認をワークスペースと同期させましょう。', + approvalMode: '承認モード', + finalApprover: '最終承認者', + notSet: '未設定', + approvalModeDescription: 'メンバーとマネージャーは TriNet と同期するように設定されています。', + approvalModeWarningTitle: '承認モードを変更しますか?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `このワークスペースの承認モードを本当に変更してもよろしいですか?TriNet に対応した各種ワークフローモードの詳細は、ヘルプサイトをご覧ください。`, + approvalModeWarningConfirm: '承認モードを変更', + approvalModes: { + basic: {label: '基本承認', description: 'すべてのユーザーが、処理と承認のために 1 人の担当者へ経費を提出します。'}, + manager: {label: 'マネージャー承認', description: '従業員は、TriNet で設定された直属のマネージャーにレポートを提出します。'}, + custom: {label: 'カスタム承認', description: 'Expensify で承認ワークフローを手動で設定します。'}, + }, }, }, }, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 891386fd47b8..107b8f440280 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -261,7 +261,6 @@ const translations: TranslationDeepObject = { reset: 'Opnieuw instellen', done: 'Klaar', more: 'Meer', - other: 'Overig', debitCard: 'Debetkaart', bankAccount: 'Bankrekening', personalBankAccount: 'Persoonlijke bankrekening', @@ -957,6 +956,8 @@ const translations: TranslationDeepObject = { personalSubtitle: 'Portemonnee', }, }, + assignedCards: 'Je Expensify Kaarten', + assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} resterend`, announcements: 'Aankondigingen', discoverSection: { title: 'Ontdekken', @@ -1036,12 +1037,6 @@ const translations: TranslationDeepObject = { setupRules: 'Uitgavenregels instellen', inviteAccountant: 'Nodig je accountant uit', }, - yourSpend: { - title: 'Je uitgaven', - awaitingApproval: 'In afwachting van goedkeuring', - repaidLast30Days: 'Terugbetaald in de afgelopen 30 dagen', - recentTransactions: ({lastFour}: {lastFour: string}) => `Recente transacties • ${lastFour}`, - }, }, allSettingsScreen: { subscription: 'Abonnement', @@ -2538,11 +2533,6 @@ ${amount} voor ${merchant} - ${date}`, frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Deze kaart is bevroren door ${person}. Neem contact op met een beheerder om deze te deblokkeren.`, spendRules: 'Bestedingsregels', editSpendRules: 'Uitgavenregels bewerken', - cardUnblocked: 'Kaart gedeblokkeerd!', - cardUnblockedDescription: 'Mogelijk wordt je de volgende keer dat je een aankoop doet gevraagd om je kaart in de lezer te steken.', - pinBlocked: 'Je kaart is geblokkeerd vanwege onjuiste pincode-invoer.', - unblock: 'Deblokkeren', - unblockCard: 'Kaart deblokkeren', }, workflowsPage: { workflowTitle: 'Uitgaven', @@ -3010,8 +3000,6 @@ ${amount} voor ${merchant} - ${date}`, mergeBlockScreen: { title: 'Werkmail kon niet worden toegevoegd', subtitle: (workEmail: string | undefined) => `We konden ${workEmail} niet toevoegen. Probeer het later opnieuw in Instellingen of chat met Concierge voor hulp.`, - workAccountClosedSubtitle: - 'De zakelijke account die aan dit e-mailadres is gekoppeld, is gesloten. Neem contact op met de beheerder van je bedrijf om het opnieuw te activeren, of meld je aan met een ander e-mailadres.', }, tasks: { testDriveAdminTask: { @@ -6718,57 +6706,6 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `HR-integraties zijn alleen beschikbaar in het Control-abonnement, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actief lid per maand.`}`, }, - approvalSubmit: { - title: 'Goedkeuringen', - description: 'Configureer centraal naar wie alle leden indienen door goedkeuringen in te schakelen.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Goedkeuringen zijn beschikbaar in de Collect- en Control-abonnementen, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, - }, - companyCardSubmit: { - title: 'Bedrijfskaarten', - description: `Neem je eigen bedrijfskaart mee naar Expensify voor automatische import, automatische categorisatie, ondersteuning voor aangepaste regels en geïntegreerde afstemming.`, - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Bedrijfskaartimport is beschikbaar in de Collect- en Control-abonnementen, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, - }, - travelSubmit: { - title: 'Expensify Travel', - description: 'Boek wereldwijd voordelige vluchten, hotels, auto’s en treinen vanuit Expensify, met duty-of-carerapportage en geïntegreerd onkostbeheer.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Expensify Travel is beschikbaar in de Collect- en Control-abonnementen, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, - }, - roles: { - title: 'Rollen', - description: 'Wijs verschillende rollen toe aan verschillende leden om de zichtbaarheid en controle naar behoefte te verhogen of te verlagen.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Rollen zijn beschikbaar in de Collect- en Control-abonnementen, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, - }, - payments: { - title: 'Betalingen', - description: 'Vergoed medewerkers rechtstreeks vanaf je zakelijke bankrekening.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Betalingen zijn beschikbaar in de abonnementen Collect en Control, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, - }, - accounting: { - title: 'Boekhouding', - description: - 'Synchroniseer categorieën, labels, belastingtarieven en meer van je boekhoudsysteem naar Expensify, en exporteer onkostendeclaraties en kaarttransacties – zonder typwerk (of tikfouten)!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Boekhouding is beschikbaar in de Collect- en Control-abonnementen, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, - }, - expensifyCard: { - title: 'Expensify Card', - description: - 'Geef bedrijfskaarten (inclusief virtuele kaarten) direct uit vanaf je eigen bankrekening voor realtime controle over uitgaven met een onbreekbare koppeling en tot 2% cashback!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `De Expensify Card is beschikbaar bij de Collect- en Control-abonnementen, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, - upgradeButton: 'Upgrade en inschakelen', - }, - invoicing: { - title: 'Facturatie', - description: 'Maak, verstuur en volg professionele facturen, allemaal in Expensify. Laat je sneller betalen met geïntegreerde betalingen en realtime inzicht.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Facturatie is beschikbaar in de Collect- en Control-abonnementen, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, - }, }, downgrade: { commonFeatures: { @@ -7094,8 +7031,8 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, description: 'Voor werknemers die onkosten bij hun werkgever willen indienen.', }, }, - description: 'Kies een abonnement dat bij je past.', - subscriptionLink: 'Meer informatie', + description: 'Kies een pakket dat bij je past. Voor een gedetailleerd overzicht van functies en prijzen, bekijk onze', + subscriptionLink: 'hulppagina voor abonnementstypen en prijzen', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `Je hebt je vastgelegd op 1 actief lid in het Control-abonnement tot je jaarlijkse abonnement afloopt op ${annualSubscriptionEndDate}. Je kunt overstappen op een pay-per-use-abonnement en downgraden naar het Collect-abonnement vanaf ${annualSubscriptionEndDate} door automatisch verlengen uit te schakelen in`, other: `Je hebt je vastgelegd op ${count} actieve leden op het Control-abonnement totdat je jaarlijkse abonnement eindigt op ${annualSubscriptionEndDate}. Je kunt overschakelen naar een betaling-per-gebruik-abonnement en downgraden naar het Collect-abonnement vanaf ${annualSubscriptionEndDate} door automatisch verlengen uit te schakelen in`, @@ -7104,32 +7041,8 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, }, hr: { title: 'HR', - connections: 'Verbindingen', subtitle: 'Koppel HR-tools en houd goedkeuringen van medewerkers gesynchroniseerd.', - connect: 'Verbinden', - syncNow: 'Nu synchroniseren', - disconnect: 'Ontkoppelen', - disconnectTitle: (providerName: string) => `${providerName} ontkoppelen`, - disconnectPrompt: (providerName: string) => `Weet je zeker dat je ${providerName} wilt ontkoppelen?`, - lastSync: (relativeDate: string) => `Laatst gesynchroniseerd ${relativeDate}`, - syncError: (providerName: string) => `Kan geen verbinding maken met ${providerName}`, - connectionDescription: (providerName: string) => `Verbind ${providerName} om goedkeuringen van werknemers gesynchroniseerd te houden met je werkruimte.`, - approvalMode: 'Goedkeuringsmodus', - finalApprover: 'Eindgoedkeurder', - notSet: 'Niet ingesteld', - approvalModeDescription: (providerName: string) => `Leden en managers zijn ingesteld om te synchroniseren met ${providerName}.`, - approvalModeWarningTitle: 'Goedkeuringsmodus wijzigen?', - approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => - `Weet je zeker dat je de goedkeuringsmodus voor deze werkruimte wilt wijzigen? Lees meer over de verschillende ${providerName}-ondersteunde workflowmodi op onze help-site.`, - approvalModeWarningConfirm: 'Goedkeuringsmodus wijzigen', - approvalModes: { - basic: {label: 'Basisgoedkeuring', description: 'Alle gebruikers dienen bij één persoon in voor verwerking en goedkeuring.'}, - manager: { - label: 'Managergoedkeuring', - description: (providerName: string) => `Werknemers dienen rapporten in bij hun direct leidinggevende die is ingesteld in ${providerName}.`, - }, - custom: {label: 'Aangepaste goedkeuring', description: 'Ik stel goedkeuringsworkflows handmatig in in Expensify.'}, - }, + settingsTitle: 'Gusto-instellingen', syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -7153,6 +7066,27 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, }, gusto: { title: 'Gusto', + approvalMode: 'Goedkeuringsmodus', + finalApprover: 'Laatste fiatteur', + notSet: 'Niet ingesteld', + approvalModeDescription: 'Leden en managers zijn ingesteld om te synchroniseren met Gusto.', + approvalModeWarningTitle: 'Goedkeuringsmodus wijzigen?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `Weet je zeker dat je de goedkeuringsmodus voor deze werkruimte wilt wijzigen? Lees meer over de verschillende Gusto-ondersteunde workflowmodi op onze help-site.`, + approvalModeWarningConfirm: 'Goedkeuringsmodus wijzigen', + approvalModes: { + basic: {label: 'Basisgoedkeuring', description: 'Alle gebruikers dienen bij één persoon in voor verwerking en goedkeuring.'}, + manager: {label: 'Goedkeuring manager', description: 'Werknemers dienen rapporten in bij hun direct leidinggevende die is ingesteld in Gusto.'}, + custom: {label: 'Aangepaste goedkeuring', description: 'Ik stel goedkeuringsworkflows handmatig in in Expensify.'}, + }, + connect: 'Verbinden', + connectionDescription: 'Verbind Gusto om goedkeuringen van werknemers gesynchroniseerd te houden met je workspace.', + syncNow: 'Nu synchroniseren', + disconnect: 'Verbreken', + lastSync: (relativeDate: string) => `Laatste synchronisatie ${relativeDate}`, + syncError: 'Kan geen verbinding maken met Gusto', + disconnectTitle: 'Verbinding met Gusto verbreken', + disconnectPrompt: 'Weet je zeker dat je de verbinding met Gusto wilt verbreken?', syncResults: { title: 'Gusto-synchronisatieresultaten', successTitle: 'Je Gusto-verbinding is succesvol gesynchroniseerd!', @@ -7165,8 +7099,33 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, }), }, }, + merge: { + approvalMode: 'Goedkeuringsmodus', + finalApprover: 'Laatste fiatteur', + }, zenefits: { title: 'TriNet', + connect: 'Verbinden', + syncNow: 'Nu synchroniseren', + disconnect: 'Verbinding verbreken', + lastSync: (relativeDate: string) => `Laatst gesynchroniseerd ${relativeDate}`, + syncError: 'Kan geen verbinding maken met TriNet', + disconnectTitle: 'Koppeling met TriNet verbreken', + disconnectPrompt: 'Weet je zeker dat je TriNet wilt loskoppelen?', + connectionDescription: 'Verbind TriNet om goedkeuringen van medewerkers synchroon te houden met je werkruimte.', + approvalMode: 'Goedkeuringsmodus', + finalApprover: 'Laatste fiatteur', + notSet: 'Niet ingesteld', + approvalModeDescription: 'Leden en managers zijn ingesteld om te synchroniseren met TriNet.', + approvalModeWarningTitle: 'Goedkeuringsmodus wijzigen?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `Weet je zeker dat je de goedkeuringsmodus voor deze workspace wilt wijzigen? Lees meer over de verschillende TriNet-ondersteunde workflowmodi op onze help-pagina.`, + approvalModeWarningConfirm: 'Goedkeuringsmodus wijzigen', + approvalModes: { + basic: {label: 'Eenvoudige goedkeuring', description: 'Alle gebruikers dienen in bij één persoon voor verwerking en goedkeuring.'}, + manager: {label: 'Goedkeuring door manager', description: 'Werknemers dienen rapporten in bij hun direct leidinggevende die is ingesteld in TriNet.'}, + custom: {label: 'Aangepaste goedkeuring', description: 'Ik stel handmatig goedkeuringsworkflows in Expensify in.'}, + }, }, }, }, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 91459e089731..af6f01873175 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -261,7 +261,6 @@ const translations: TranslationDeepObject = { reset: 'Reset', done: 'Gotowe', more: 'Więcej', - other: 'Inne', debitCard: 'Karta debetowa', bankAccount: 'Konto bankowe', personalBankAccount: 'Prywatne konto bankowe', @@ -959,6 +958,8 @@ const translations: TranslationDeepObject = { personalSubtitle: 'Portfel', }, }, + assignedCards: 'Twoje Karty Expensify', + assignedCardsRemaining: ({amount}: {amount: string}) => `Pozostało ${amount}`, announcements: 'Ogłoszenia', discoverSection: { title: 'Odkryj', @@ -1038,12 +1039,6 @@ const translations: TranslationDeepObject = { setupRules: 'Skonfiguruj zasady wydatków', inviteAccountant: 'Zaproś swojego księgowego', }, - yourSpend: { - title: 'Twoje wydatki', - awaitingApproval: 'Oczekuje na zatwierdzenie', - repaidLast30Days: 'Spłacono w ciągu ostatnich 30 dni', - recentTransactions: ({lastFour}: {lastFour: string}) => `Ostatnie transakcje • ${lastFour}`, - }, }, allSettingsScreen: { subscription: 'Subskrypcja', @@ -2534,11 +2529,6 @@ ${amount} dla ${merchant} - ${date}`, frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Ta karta została zamrożona przez ${person}. Skontaktuj się z administratorem, aby ją odmrozić.`, spendRules: 'Zasady wydatków', editSpendRules: 'Edytuj zasady wydatków', - cardUnblocked: 'Karta odblokowana!', - cardUnblockedDescription: 'Przy następnych zakupach możesz zostać poproszony(-a) o włożenie karty do czytnika.', - pinBlocked: 'Twoja karta została zablokowana z powodu błędnego wprowadzenia kodu PIN.', - unblock: 'Odblokuj', - unblockCard: 'Odblokuj kartę', }, workflowsPage: { workflowTitle: 'Wydatki', @@ -3003,8 +2993,6 @@ ${amount} dla ${merchant} - ${date}`, mergeBlockScreen: { title: 'Nie udało się dodać służbowego adresu e-mail', subtitle: (workEmail: string | undefined) => `Nie udało się dodać ${workEmail}. Spróbuj ponownie później w Ustawieniach lub porozmawiaj z Concierge, aby uzyskać pomoc.`, - workAccountClosedSubtitle: - 'Konto służbowe powiązane z tym adresem e‑mail jest zamknięte. Skontaktuj się z administratorem w swojej firmie, żeby je ponownie aktywować, albo zarejestruj się przy użyciu innego adresu e‑mail.', }, tasks: { testDriveAdminTask: { @@ -6711,58 +6699,6 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Integracje HR są dostępne tylko w planie Control, zaczynającym się od ${formattedPrice} ${hasTeam2025Pricing ? `za osobę miesięcznie.` : `za aktywnego członka miesięcznie.`}`, }, - approvalSubmit: { - title: 'Zatwierdzenia', - description: 'Skonfiguruj centralnie, do kogo wszyscy członkowie składają raporty, włączając zatwierdzanie.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Zatwierdzanie jest dostępne w planach Collect i Control, zaczynając od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`}`, - }, - companyCardSubmit: { - title: 'Firmowe karty', - description: `Dodaj swoją firmową kartę do Expensify, aby uzyskać automatyczny import, automatyczną kategoryzację, obsługę konfigurowalnych reguł i zintegrowane uzgadnianie.`, - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Importowanie kart firmowych jest dostępne w planach Collect i Control, zaczynających się od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`}`, - }, - travelSubmit: { - title: 'Podróże Expensify', - description: 'Rezerwuj na całym świecie tańsze loty, hotele, samochody i pociągi bezpośrednio w Expensify, z raportami duty of care i zintegrowanym zarządzaniem wydatkami.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Expensify Travel jest dostępny w planach Collect i Control, zaczynając od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`}`, - }, - roles: { - title: 'Role', - description: 'Przypisz różne role różnym członkom, aby w razie potrzeby zwiększyć lub zmniejszyć poziom widoczności i kontroli.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Role są dostępne w planach Collect i Control, zaczynając od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`}`, - }, - payments: { - title: 'Płatności', - description: 'Zwracaj pracownikom wydatki bezpośrednio z firmowego konta bankowego.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Płatności są dostępne w planach Collect i Control, zaczynając od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`}`, - }, - accounting: { - title: 'Księgowość', - description: - 'Synchronizuj kategorie, tagi, stawki podatkowe i więcej z Twojego systemu księgowego do Expensify, a także eksportuj raporty wydatków i transakcje kartą – bez przepisywania (i bez literówek)!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Księgowość jest dostępna w planach Collect i Control, zaczynając od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`}`, - }, - expensifyCard: { - title: 'Karta Expensify', - description: - 'Wydawaj firmowe karty (w tym karty wirtualne) bezpośrednio z własnego konta bankowego, aby zyskać kontrolę nad wydatkami w czasie rzeczywistym dzięki nierozerwalnemu połączeniu i nawet do 2% zwrotu gotówki!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Karta Expensify jest dostępna w planach Collect i Control, zaczynając od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`}`, - upgradeButton: 'Ulepsz i włącz', - }, - invoicing: { - title: 'Fakturowanie', - description: - 'Twórz, wysyłaj i śledź profesjonalne faktury — wszystko w Expensify. Otrzymuj płatności szybciej dzięki zintegrowanym płatnościom i wglądowi w czasie rzeczywistym.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Fakturowanie jest dostępne w planach Collect i Control, zaczynając od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`}`, - }, }, downgrade: { commonFeatures: { @@ -7089,8 +7025,8 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, description: 'Dla pracowników, którzy chcą przesyłać wydatki do pracodawcy.', }, }, - description: 'Wybierz plan odpowiedni dla siebie.', - subscriptionLink: 'Dowiedz się więcej', + description: 'Wybierz plan odpowiedni dla siebie. Szczegółową listę funkcji i cen znajdziesz w naszej', + subscriptionLink: 'strona pomocy dotycząca typów planów i cen', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `Zobowiązałeś(-aś) się do 1 aktywnego członka w planie Control do końca rocznej subskrypcji ${annualSubscriptionEndDate}. Możesz przejść na subskrypcję z rozliczaniem za użycie i zmienić plan na Collect od ${annualSubscriptionEndDate}, wyłączając automatyczne odnawianie w`, other: `Zobowiązałeś(-aś) się do ${count} aktywnych członków w planie Control do końca rocznej subskrypcji ${annualSubscriptionEndDate}. Możesz przejść na subskrypcję płatną za użycie i zmienić plan na Collect od ${annualSubscriptionEndDate}, wyłączając automatyczne odnawianie w`, @@ -7099,32 +7035,8 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, }, hr: { title: 'HR', - connections: 'Połączenia', subtitle: 'Połącz narzędzia HR i utrzymuj zgody pracowników w synchronizacji.', - connect: 'Połącz', - syncNow: 'Synchronizuj teraz', - disconnect: 'Odłącz', - disconnectTitle: (providerName: string) => `Odłącz ${providerName}`, - disconnectPrompt: (providerName: string) => `Czy na pewno chcesz odłączyć ${providerName}?`, - lastSync: (relativeDate: string) => `Ostatnia synchronizacja ${relativeDate}`, - syncError: (providerName: string) => `Nie można połączyć z ${providerName}`, - connectionDescription: (providerName: string) => `Połącz ${providerName}, aby synchronizować akceptacje pracowników z Twoim miejscem pracy.`, - approvalMode: 'Tryb zatwierdzania', - finalApprover: 'Ostateczny zatwierdzający', - notSet: 'Nie ustawiono', - approvalModeDescription: (providerName: string) => `Członkowie i menedżerowie są skonfigurowani do synchronizacji z ${providerName}.`, - approvalModeWarningTitle: 'Zmienić tryb zatwierdzania?', - approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => - `Czy na pewno chcesz zmienić tryb zatwierdzania dla tego obszaru roboczego? Dowiedz się więcej o różnych trybach przepływu pracy obsługiwanych przez ${providerName} w naszej witrynie pomocy.`, - approvalModeWarningConfirm: 'Zmień tryb zatwierdzania', - approvalModes: { - basic: {label: 'Podstawowe zatwierdzanie', description: 'Wszyscy użytkownicy przesyłają wydatki do jednej osoby w celu ich przetworzenia i zatwierdzenia.'}, - manager: { - label: 'Zatwierdzanie przez menedżera', - description: (providerName: string) => `Pracownicy przesyłają raporty do swojego bezpośredniego menedżera skonfigurowanego w ${providerName}.`, - }, - custom: {label: 'Niestandardowe zatwierdzanie', description: 'Ręcznie skonfiguruję procesy zatwierdzania w Expensify.'}, - }, + settingsTitle: 'Ustawienia Gusto', syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -7148,6 +7060,27 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, }, gusto: { title: 'Gusto', + approvalMode: 'Tryb zatwierdzania', + finalApprover: 'Ostateczny zatwierdzający', + notSet: 'Nie ustawiono', + approvalModeDescription: 'Członkowie i menedżerowie są skonfigurowani do synchronizacji z Gusto.', + approvalModeWarningTitle: 'Zmienić tryb zatwierdzania?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `Czy na pewno chcesz zmienić tryb zatwierdzania dla tego obszaru roboczego? Dowiedz się więcej o różnych trybach przepływu pracy obsługiwanych przez Gusto w naszej witrynie pomocy.`, + approvalModeWarningConfirm: 'Zmień tryb zatwierdzania', + approvalModes: { + basic: {label: 'Podstawowa akceptacja', description: 'Wszyscy użytkownicy przesyłają wydatki do jednej osoby w celu ich przetworzenia i zatwierdzenia.'}, + manager: {label: 'Zatwierdzenie przez menedżera', description: 'Pracownicy przesyłają raporty do swojego bezpośredniego menedżera skonfigurowanego w Gusto.'}, + custom: {label: 'Niestandardowa akceptacja', description: 'Ręcznie skonfiguruję procesy zatwierdzania w Expensify.'}, + }, + connect: 'Połącz', + connectionDescription: 'Połącz Gusto, aby synchronizować akceptacje pracowników z Twoim miejscem pracy.', + syncNow: 'Synchronizuj teraz', + disconnect: 'Odłącz', + lastSync: (relativeDate: string) => `Ostatnia synchronizacja: ${relativeDate}`, + syncError: 'Nie można połączyć się z Gusto', + disconnectTitle: 'Odłącz Gusto', + disconnectPrompt: 'Czy na pewno chcesz odłączyć Gusto?', syncResults: { title: 'Wyniki synchronizacji Gusto', successTitle: 'Pomyślnie zsynchronizowano Twoje połączenie z Gusto!', @@ -7160,8 +7093,33 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, }), }, }, + merge: { + approvalMode: 'Tryb zatwierdzania', + finalApprover: 'Ostateczny zatwierdzający', + }, zenefits: { title: 'TriNet', + connect: 'Połącz', + syncNow: 'Synchronizuj teraz', + disconnect: 'Odłącz', + lastSync: (relativeDate: string) => `Ostatnia synchronizacja: ${relativeDate}`, + syncError: 'Nie można połączyć się z TriNet', + disconnectTitle: 'Odłącz TriNet', + disconnectPrompt: 'Czy na pewno chcesz odłączyć TriNet?', + connectionDescription: 'Połącz TriNet, żeby utrzymywać zatwierdzenia pracowników w synchronizacji z twoim workspace’em.', + approvalMode: 'Tryb zatwierdzania', + finalApprover: 'Ostateczny zatwierdzający', + notSet: 'Nie ustawiono', + approvalModeDescription: 'Członkowie i menedżerowie są skonfigurowani do synchronizacji z TriNet.', + approvalModeWarningTitle: 'Zmienić tryb zatwierdzania?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `Czy na pewno chcesz zmienić tryb zatwierdzania dla tego workspace? Dowiedz się więcej o różnych trybach workflow obsługiwanych przez TriNet na naszej stronie pomocy.`, + approvalModeWarningConfirm: 'Zmień tryb akceptacji', + approvalModes: { + basic: {label: 'Podstawowa akceptacja', description: 'Wszyscy użytkownicy przesyłają wydatki do jednej osoby w celu ich przetworzenia i zatwierdzenia.'}, + manager: {label: 'Akceptacja przez menedżera', description: 'Pracownicy składają raporty do swojego bezpośredniego przełożonego skonfigurowanego w TriNet.'}, + custom: {label: 'Niestandardowe zatwierdzanie', description: 'Ręcznie skonfiguruję przepływy akceptacji w Expensify.'}, + }, }, }, }, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 535659f23f58..75592a0d4982 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -261,7 +261,6 @@ const translations: TranslationDeepObject = { reset: 'Redefinir', done: 'Concluído', more: 'Mais', - other: 'Outro', debitCard: 'Cartão de débito', bankAccount: 'Conta bancária', personalBankAccount: 'Conta bancária pessoal', @@ -957,6 +956,8 @@ const translations: TranslationDeepObject = { personalSubtitle: 'Carteira', }, }, + assignedCards: 'Seus Cartões Expensify', + assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} restante`, announcements: 'Comunicados', discoverSection: { title: 'Descobrir', @@ -1036,12 +1037,6 @@ const translations: TranslationDeepObject = { setupRules: 'Configurar regras de gasto', inviteAccountant: 'Convide seu contador', }, - yourSpend: { - title: 'Seus gastos', - awaitingApproval: 'Aguardando aprovação', - repaidLast30Days: 'Reembolsado nos últimos 30 dias', - recentTransactions: ({lastFour}: {lastFour: string}) => `Transações recentes • ${lastFour}`, - }, }, allSettingsScreen: { subscription: 'Assinatura', @@ -2532,11 +2527,6 @@ ${amount} para ${merchant} - ${date}`, frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Este cartão foi bloqueado por ${person}. Entre em contato com um administrador para desbloqueá-lo.`, spendRules: 'Regras de gasto', editSpendRules: 'Editar regras de gastos', - cardUnblocked: 'Cartão desbloqueado!', - cardUnblockedDescription: 'Pode ser que você seja solicitado a inserir o seu cartão no leitor na próxima vez que fizer uma compra.', - pinBlocked: 'Seu cartão foi bloqueado devido a tentativas de PIN incorretas.', - unblock: 'Desbloquear', - unblockCard: 'Desbloquear cartão', }, workflowsPage: { workflowTitle: 'Gastos', @@ -3004,8 +2994,6 @@ ${amount} para ${merchant} - ${date}`, title: 'Não foi possível adicionar o e-mail de trabalho', subtitle: (workEmail: string | undefined) => `Não foi possível adicionar ${workEmail}. Tente novamente mais tarde em Configurações ou converse com o Concierge para obter orientação.`, - workAccountClosedSubtitle: - 'A conta de trabalho associada a este e-mail foi encerrada. Entre em contato com o administrador da sua empresa para reativá-la ou inscreva-se com um e-mail diferente.', }, tasks: { testDriveAdminTask: { @@ -6717,57 +6705,6 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Integrações de RH estão disponíveis apenas no plano Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, }, - approvalSubmit: { - title: 'Aprovações', - description: 'Configure centralmente para quem todos os membros enviam relatórios ativando as aprovações.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `As aprovações estão disponíveis nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, - }, - companyCardSubmit: { - title: 'Cartões corporativos', - description: `Traga seu próprio cartão corporativo para o Expensify para obter importação automática, categorização automática, suporte a regras personalizáveis e reconciliação integrada.`, - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `A importação de cartão corporativo está disponível nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, - }, - travelSubmit: { - title: 'Expensify Travel', - description: 'Reserve voos, hotéis, carros e trens com desconto no mundo todo, diretamente pelo Expensify, com relatórios de duty of care e gestão de despesas integrada.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `O Expensify Travel está disponível nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, - }, - roles: { - title: 'Funções', - description: 'Defina funções diferentes para membros diferentes para aumentar ou reduzir a visibilidade e o controle conforme apropriado.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Funções estão disponíveis nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, - }, - payments: { - title: 'Pagamentos', - description: 'Reembolse funcionários diretamente da conta bancária da sua empresa.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Os pagamentos estão disponíveis nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, - }, - accounting: { - title: 'Contabilidade', - description: - 'Sincronize categorias, tags, taxas de imposto e muito mais do seu sistema contábil para o Expensify, além de exportar relatórios de despesas e transações de cartão – sem digitação (ou erros de digitação)!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `O módulo de contabilidade está disponível nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, - }, - expensifyCard: { - title: 'Cartão Expensify', - description: - 'Emita cartões corporativos (incluindo cartões virtuais) diretamente da sua própria conta bancária para ter controle de gastos em tempo real com uma conexão inquebrável e até 2% de cashback!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `O Cartão Expensify está disponível nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, - upgradeButton: 'Fazer upgrade e ativar', - }, - invoicing: { - title: 'Faturamento', - description: 'Crie, envie e acompanhe faturas profissionais, tudo dentro do Expensify. Receba mais rápido com pagamentos integrados e visibilidade em tempo real.', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `O faturamento está disponível nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, - }, }, downgrade: { commonFeatures: { @@ -7094,8 +7031,8 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, description: 'Para funcionários que desejam enviar despesas ao empregador.', }, }, - description: 'Escolha o plano ideal para você.', - subscriptionLink: 'Saiba mais', + description: 'Escolha o plano ideal para você. Para ver a lista detalhada de recursos e preços, confira nosso', + subscriptionLink: 'página de ajuda sobre tipos de plano e preços', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `Você se comprometeu com 1 membro ativo no plano Control até o fim da sua assinatura anual em ${annualSubscriptionEndDate}. Você pode mudar para a assinatura pré-paga por uso e fazer downgrade para o plano Collect a partir de ${annualSubscriptionEndDate}, desativando a renovação automática em`, other: `Você se comprometeu com ${count} membros ativos no plano Control até o fim da sua assinatura anual em ${annualSubscriptionEndDate}. Você pode mudar para a assinatura pós-paga e fazer downgrade para o plano Collect a partir de ${annualSubscriptionEndDate} desativando a renovação automática em`, @@ -7104,32 +7041,8 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, }, hr: { title: 'RH', - connections: 'Conexões', subtitle: 'Conecte ferramentas de RH e mantenha as aprovações de funcionários em sincronia.', - connect: 'Conectar', - syncNow: 'Sincronizar agora', - disconnect: 'Desconectar', - disconnectTitle: (providerName: string) => `Desconectar ${providerName}`, - disconnectPrompt: (providerName: string) => `Tem certeza de que deseja desconectar ${providerName}?`, - lastSync: (relativeDate: string) => `Última sincronização ${relativeDate}`, - syncError: (providerName: string) => `Não é possível conectar ao ${providerName}`, - connectionDescription: (providerName: string) => `Conecte ${providerName} para manter as aprovações de funcionários sincronizadas com seu workspace.`, - approvalMode: 'Modo de aprovação', - finalApprover: 'Aprovador final', - notSet: 'Não definido', - approvalModeDescription: (providerName: string) => `Membros e gerentes estão configurados para sincronizar com ${providerName}.`, - approvalModeWarningTitle: 'Alterar modo de aprovação?', - approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => - `Tem certeza de que deseja alterar o modo de aprovação deste workspace? Saiba mais sobre os diferentes modos de fluxo de trabalho com ${providerName} em nosso site de ajuda.`, - approvalModeWarningConfirm: 'Alterar modo de aprovação', - approvalModes: { - basic: {label: 'Aprovação básica', description: 'Todos os usuários enviam para uma única pessoa para processamento e aprovação.'}, - manager: { - label: 'Aprovação do gerente', - description: (providerName: string) => `Os funcionários enviam relatórios ao gestor direto configurado no ${providerName}.`, - }, - custom: {label: 'Aprovação personalizada', description: 'Vou configurar manualmente os fluxos de aprovação no Expensify.'}, - }, + settingsTitle: 'Configurações do Gusto', syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -7153,6 +7066,27 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, }, gusto: { title: 'Gusto', + approvalMode: 'Modo de aprovação', + finalApprover: 'Aprovador final', + notSet: 'Não definido', + approvalModeDescription: 'Membros e gerentes estão configurados para sincronizar com o Gusto.', + approvalModeWarningTitle: 'Alterar modo de aprovação?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `Tem certeza de que deseja alterar o modo de aprovação deste workspace? Saiba mais sobre os diferentes modos de fluxo de trabalho com Gusto em nosso site de ajuda.`, + approvalModeWarningConfirm: 'Alterar modo de aprovação', + approvalModes: { + basic: {label: 'Aprovação básica', description: 'Todos os usuários enviam para uma única pessoa para processamento e aprovação.'}, + manager: {label: 'Aprovação do gerente', description: 'Os funcionários enviam relatórios ao gestor direto configurado no Gusto.'}, + custom: {label: 'Aprovação personalizada', description: 'Vou configurar manualmente os fluxos de aprovação no Expensify.'}, + }, + connect: 'Conectar', + connectionDescription: 'Conecte o Gusto para manter as aprovações de funcionários sincronizadas com seu workspace.', + syncNow: 'Sincronizar agora', + disconnect: 'Desconectar', + lastSync: (relativeDate: string) => `Última sincronização ${relativeDate}`, + syncError: 'Não é possível conectar ao Gusto', + disconnectTitle: 'Desconectar Gusto', + disconnectPrompt: 'Tem certeza de que deseja desconectar o Gusto?', syncResults: { title: 'Resultados da sincronização com Gusto', successTitle: 'Sua conexão com o Gusto foi sincronizada com sucesso!', @@ -7165,8 +7099,33 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, }), }, }, + merge: { + approvalMode: 'Modo de aprovação', + finalApprover: 'Aprovador final', + }, zenefits: { title: 'TriNet', + connect: 'Conectar', + syncNow: 'Sincronizar agora', + disconnect: 'Desconectar', + lastSync: (relativeDate: string) => `Última sincronização ${relativeDate}`, + syncError: 'Não é possível conectar ao TriNet', + disconnectTitle: 'Desconectar TriNet', + disconnectPrompt: 'Tem certeza de que quer desconectar a TriNet?', + connectionDescription: 'Conecte o TriNet para manter as aprovações de funcionários sincronizadas com o seu espaço de trabalho.', + approvalMode: 'Modo de aprovação', + finalApprover: 'Aprovador final', + notSet: 'Não definido', + approvalModeDescription: 'Os membros e gerentes estão configurados para sincronizar com o TriNet.', + approvalModeWarningTitle: 'Alterar modo de aprovação?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `Tem certeza de que deseja alterar o modo de aprovação deste espaço de trabalho? Saiba mais sobre os diferentes modos de fluxo de trabalho habilitados pelo TriNet no nosso site de ajuda.`, + approvalModeWarningConfirm: 'Alterar modo de aprovação', + approvalModes: { + basic: {label: 'Aprovação básica', description: 'Todos os usuários enviam para uma única pessoa para processamento e aprovação.'}, + manager: {label: 'Aprovação do gerente', description: 'Os funcionários enviam relatórios para o gerente direto configurado no TriNet.'}, + custom: {label: 'Aprovação personalizada', description: 'Vou configurar manualmente fluxos de aprovação no Expensify.'}, + }, }, }, }, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 3c9e8607e50e..fe9f58ee820a 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -261,7 +261,6 @@ const translations: TranslationDeepObject = { reset: '重置', done: '完成', more: '更多', - other: '其他', debitCard: '借记卡', bankAccount: '银行账户', personalBankAccount: '个人银行账户', @@ -925,6 +924,8 @@ const translations: TranslationDeepObject = { personalSubtitle: '钱包', }, }, + assignedCards: '你的 Expensify 卡', + assignedCardsRemaining: ({amount}: {amount: string}) => `剩余 ${amount}`, announcements: '公告', discoverSection: { title: '发现', @@ -1004,7 +1005,6 @@ const translations: TranslationDeepObject = { setupRules: '设置消费规则', inviteAccountant: '邀请你的会计', }, - yourSpend: {title: '您的支出', awaitingApproval: '等待审批', repaidLast30Days: '过去30天内已偿还', recentTransactions: ({lastFour}: {lastFour: string}) => `最近交易 • ${lastFour}`}, }, allSettingsScreen: { subscription: '订阅', @@ -2466,11 +2466,6 @@ ${amount},商户:${merchant} - 日期:${date}`, frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `此卡已被${person}冻结。请联系管理员解冻。`, spendRules: '支出规则', editSpendRules: '编辑支出规则', - cardUnblocked: '卡已解除冻结!', - cardUnblockedDescription: '下次消费时,系统可能会提示你将银行卡插入读卡器。', - pinBlocked: '由于多次输入错误 PIN,您的银行卡已被停用。', - unblock: '取消屏蔽', - unblockCard: '解除卡片冻结', }, workflowsPage: { workflowTitle: '支出', @@ -2929,7 +2924,6 @@ ${amount},商户:${merchant} - 日期:${date}`, mergeBlockScreen: { title: '无法添加工作邮箱', subtitle: (workEmail: string | undefined) => `我们无法添加 ${workEmail}。请稍后在设置中重试,或与 Concierge 聊天以获取指导。`, - workAccountClosedSubtitle: '与此邮箱关联的工作账户已被关闭。请联系您公司的管理员以重新激活该账户,或使用其他邮箱注册。', }, tasks: { testDriveAdminTask: { @@ -6543,55 +6537,6 @@ ${reportName} onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `人力资源集成功能仅在 Control 方案中提供,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, }, - approvalSubmit: { - title: '审批', - description: '启用审批功能,以集中配置所有成员向谁提交。', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `审批功能适用于 Collect 和 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, - }, - companyCardSubmit: { - title: '公司卡枚', - description: `将你自己的公司卡片连接到 Expensify,可享受自动导入、自动分类、自定义规则支持以及集成对账功能。`, - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `公司信用卡导入功能适用于 Collect 和 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, - }, - travelSubmit: { - title: 'Expensify 差旅', - description: '通过 Expensify 在全球预订折扣机票、酒店、租车和火车,并享受差旅关怀报告和集成费用管理。', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Expensify Travel 适用于 Collect 和 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, - }, - roles: { - title: '角色', - description: '为不同成员分配不同角色,以根据需要提高或降低可见性和控制权。', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `角色可在 Collect 和 Control 方案中使用,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, - }, - payments: { - title: '付款', - description: '直接从您的企业银行账户向员工报销。', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `付款功能适用于 Collect 和 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, - }, - accounting: { - title: '会计', - description: '从您的会计系统同步类别、标签、税率等信息到 Expensify,并导出费用报表和卡片交易记录——全程无需手动输入,也不用担心输入错误!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `会计功能适用于 Collect 和 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, - }, - expensifyCard: { - title: 'Expensify 卡', - description: '直接从您自己的银行账户发放公司卡(包括虚拟卡),通过牢不可破的连接实现实时消费管控,并享受最高 2% 返现!', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `Expensify 卡适用于 Collect 和 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, - upgradeButton: '升级并启用', - }, - invoicing: { - title: '开具发票', - description: '在 Expensify 中创建、发送并跟踪专业发票。通过集成付款和实时可见性,让收款更快捷。', - onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => - `开票功能适用于 Collect 和 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, - }, }, downgrade: { commonFeatures: { @@ -6911,8 +6856,8 @@ ${reportName} description: '适用于希望向雇主提交费用的员工。', }, }, - description: '选择适合你的方案。', - subscriptionLink: '了解详情', + description: '选择适合您的方案。要查看详细的功能和价格列表,请访问我们的', + subscriptionLink: '方案类型和价格帮助页面', lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ one: `在 Control 方案中,你已承诺在年度订阅到期(${annualSubscriptionEndDate})前保持 1 位活跃成员。你可以在 ${annualSubscriptionEndDate} 起关闭自动续订,以改为按使用付费订阅并降级到 Collect 方案,操作入口在`, other: `在年费订阅于${annualSubscriptionEndDate}结束之前,你已承诺在 Control 方案中保留 ${count} 名活跃成员。你可以在${annualSubscriptionEndDate}起,通过关闭自动续订,改为按使用量付费订阅并降级到 Collect 方案,操作入口在`, @@ -6921,32 +6866,8 @@ ${reportName} }, hr: { title: '人力资源', - connections: '连接', subtitle: '连接人力资源工具,保持员工审批同步。', - connect: '连接', - syncNow: '立即同步', - disconnect: '断开连接', - disconnectTitle: (providerName: string) => `断开 ${providerName}`, - disconnectPrompt: (providerName: string) => `确定要断开 ${providerName} 吗?`, - lastSync: (relativeDate: string) => `上次同步 ${relativeDate}`, - syncError: (providerName: string) => `无法连接到 ${providerName}`, - connectionDescription: (providerName: string) => `连接 ${providerName},以在您的工作区中同步员工审批。`, - approvalMode: '审批模式', - finalApprover: '最终审批人', - notSet: '未设置', - approvalModeDescription: (providerName: string) => `成员和管理员已设置为与 ${providerName} 同步。`, - approvalModeWarningTitle: '更改审批模式?', - approvalModeWarningPrompt: (providerName: string, helpSiteURL: string) => - `您确定要更改此工作区的审批模式吗?在我们的帮助网站中了解更多关于不同 ${providerName} 启用的工作流模式的信息。`, - approvalModeWarningConfirm: '更改审批模式', - approvalModes: { - basic: {label: '基本审批', description: '所有用户都提交给同一个人进行处理和审批。'}, - manager: { - label: '经理审批', - description: (providerName: string) => `员工会将报销单提交给在 ${providerName} 中配置的直属经理。`, - }, - custom: {label: '自定义审批', description: '我将在 Expensify 中手动设置审批工作流程。'}, - }, + settingsTitle: 'Gusto 设置', syncStageName: ({stage}: SyncStageNameConnectionsParams) => { switch (stage) { case 'gustoSyncTitle': @@ -6970,6 +6891,27 @@ ${reportName} }, gusto: { title: 'Gusto', + approvalMode: '审批模式', + finalApprover: '最终审批人', + notSet: '未设置', + approvalModeDescription: '成员和管理员已设置为与 Gusto 同步。', + approvalModeWarningTitle: '更改审批模式?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `您确定要更改此工作区的审批模式吗?在我们的帮助网站中了解更多关于不同 Gusto 启用的工作流模式的信息。`, + approvalModeWarningConfirm: '更改审批模式', + approvalModes: { + basic: {label: '基础审批', description: '所有用户都提交给同一个人进行处理和审批。'}, + manager: {label: '经理审批', description: '员工会将报销单提交给在 Gusto 中配置的直属经理。'}, + custom: {label: '自定义审批', description: '我将在 Expensify 中手动设置审批工作流程。'}, + }, + connect: '连接', + connectionDescription: '连接 Gusto,以在您的工作区中同步员工审批。', + syncNow: '立即同步', + disconnect: '断开连接', + lastSync: (relativeDate: string) => `上次同步时间:${relativeDate}`, + syncError: '无法连接到 Gusto', + disconnectTitle: '断开 Gusto', + disconnectPrompt: '确定要断开与 Gusto 的连接吗?', syncResults: { title: 'Gusto 同步结果', successTitle: '已成功同步你的 Gusto 连接!', @@ -6982,8 +6924,33 @@ ${reportName} }), }, }, + merge: { + approvalMode: '审批模式', + finalApprover: '最终审批人', + }, zenefits: { title: 'TriNet', + connect: '连接', + syncNow: '立即同步', + disconnect: '断开连接', + lastSync: (relativeDate: string) => `上次同步时间:${relativeDate}`, + syncError: '无法连接到 TriNet', + disconnectTitle: '断开 TriNet 连接', + disconnectPrompt: '确定要断开 TriNet 吗?', + connectionDescription: '连接 TriNet,将员工审批与你的工作区保持同步。', + approvalMode: '审批模式', + finalApprover: '最终审批人', + notSet: '未设置', + approvalModeDescription: '成员和管理者已设置为与 TriNet 同步。', + approvalModeWarningTitle: '更改审批模式?', + approvalModeWarningPrompt: (helpSiteURL: string) => + `您确定要更改此工作区的审批模式吗?在我们的帮助网站中了解更多关于不同 TriNet 启用的工作流模式的信息。`, + approvalModeWarningConfirm: '更改审批模式', + approvalModes: { + basic: {label: '基础审批', description: '所有用户都提交给同一个人进行处理和审批。'}, + manager: {label: '经理审批', description: '员工会将报销报告提交给在 TriNet 中配置的直属经理。'}, + custom: {label: '自定义审批', description: '我会在 Expensify 中手动设置审批流程。'}, + }, }, }, }, From 8298d831a3f7b199ef4a113f04ab3d150706a992 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Thu, 21 May 2026 08:55:36 +0000 Subject: [PATCH 21/31] Add duplicateFeedModal translations from Polyglot Parrot Co-authored-by: Joel Davies --- src/languages/de.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + 9 files changed, 9 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 8ac304f70aad..ce8a7c918c24 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -5351,6 +5351,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU `Ups! Du hast ein einzelnes Feld („${duplicateColumn}“) mehreren Spalten zugeordnet. Bitte überprüfe die Zuordnung und versuche es erneut.`, }, fileImportDescription: 'Eine manuelle Option, falls Ihre Bank keinen Feed senden kann.', + duplicateFeedModal: {title: 'Karten-Feed bereits verbunden', prompt: 'Sie können denselben Kartenfeed nicht zweimal zu demselben Workspace hinzufügen.'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: 'Letzter Tag des Monats', diff --git a/src/languages/es.ts b/src/languages/es.ts index 59d55843e76c..12aa6d23e25c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5175,6 +5175,7 @@ ${amount} para ${merchant} - ${date}`, requiredColumns: (missingColumns: string) => `Por favor, asigna una columna a cada uno de los atributos: ${missingColumns}.`, duplicateColumns: (duplicateColumn: string) => `¡Ups! Has asignado un solo campo ("${duplicateColumn}") a múltiples columnas. Por favor, revisa y vuelve a intentarlo.`, }, + duplicateFeedModal: {title: 'Feed de tarjeta ya conectado', prompt: 'No puedes añadir el mismo flujo de tarjeta al mismo espacio de trabajo dos veces.'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: 'Último día del mes', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 4ec6dad932ff..3e7434452666 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5359,6 +5359,7 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. duplicateColumns: (duplicateColumn: string) => `Oups ! Vous avez associé un seul champ (« ${duplicateColumn} ») à plusieurs colonnes. Veuillez vérifier et réessayer.`, }, fileImportDescription: 'Une option manuelle si votre banque ne peut pas envoyer de flux.', + duplicateFeedModal: {title: 'Flux de carte déjà connecté', prompt: 'Vous ne pouvez pas ajouter deux fois le même flux de carte au même espace de travail.'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: 'Dernier jour du mois', diff --git a/src/languages/it.ts b/src/languages/it.ts index 7caa746dda7c..ffe035c6bd49 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -5333,6 +5333,7 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. duplicateColumns: (duplicateColumn: string) => `Ops! Hai associato un singolo campo ("${duplicateColumn}") a più colonne. Controlla e riprova.`, }, fileImportDescription: "Un'opzione manuale se la tua banca non può inviare un feed.", + duplicateFeedModal: {title: 'Feed carta già collegato', prompt: 'Non puoi aggiungere due volte lo stesso feed della carta allo stesso spazio di lavoro.'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: 'Ultimo giorno del mese', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c191fbf38b2f..32477988cd59 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5283,6 +5283,7 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO duplicateColumns: (duplicateColumn: string) => `おっと!1 つのフィールド("${duplicateColumn}")を複数の列にマッピングしています。確認して、もう一度お試しください。`, }, fileImportDescription: '銀行からフィードを送信できない場合の手動オプションです。', + duplicateFeedModal: {title: 'カードフィードはすでに接続されています', prompt: '同じカードフィードを同じワークスペースに二重に追加することはできません。'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: '月末最終日', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 107b8f440280..f3339eef11bd 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -5323,6 +5323,7 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ `Oeps! Je hebt één enkel veld (“${duplicateColumn}”) aan meerdere kolommen gekoppeld. Controleer dit en probeer het opnieuw.`, }, fileImportDescription: 'Een handmatige optie als je bank geen feed kan versturen.', + duplicateFeedModal: {title: 'Kaartfeed al verbonden', prompt: 'Je kunt dezelfde kaartfeed niet twee keer aan dezelfde workspace toevoegen.'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: 'Laatste dag van de maand', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index af6f01873175..bf3de127c952 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -5314,6 +5314,7 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy duplicateColumns: (duplicateColumn: string) => `Ups! Przypisałeś jedno pole („${duplicateColumn}”) do wielu kolumn. Sprawdź i spróbuj ponownie.`, }, fileImportDescription: 'Ręczna opcja, jeśli Twój bank nie może wysyłać kanału danych.', + duplicateFeedModal: {title: 'Kanał karty jest już połączony', prompt: 'Nie możesz dodać tego samego źródła danych karty do tego samego obszaru roboczego dwa razy.'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: 'Ostatni dzień miesiąca', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 75592a0d4982..89410dc8a1f7 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -5316,6 +5316,7 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS duplicateColumns: (duplicateColumn: string) => `Ops! Você mapeou um único campo ("${duplicateColumn}") para várias colunas. Revise e tente novamente.`, }, fileImportDescription: 'Uma opção manual caso seu banco não possa enviar um feed.', + duplicateFeedModal: {title: 'Feed do cartão já conectado', prompt: 'Você não pode adicionar o mesmo feed de cartão ao mesmo workspace duas vezes.'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: 'Último dia do mês', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index fe9f58ee820a..aa3b0723a04b 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5198,6 +5198,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM duplicateColumns: (duplicateColumn: string) => `哎呀!你已将单个字段(“${duplicateColumn}”)映射到了多个列。请检查后重试。`, }, fileImportDescription: '如果您的银行无法发送数据馈送时的手动选项。', + duplicateFeedModal: {title: '卡片流水已连接', prompt: '您不能将同一张卡片流水两次添加到同一个工作区。'}, }, statementCloseDate: { [CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: '月末最后一天', From 6b7e6138c0eeff84d3d2e44554143465526d02e7 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Thu, 21 May 2026 09:33:31 +0000 Subject: [PATCH 22/31] Replace deprecated ConfirmModal with useConfirmModal hook Fixes ESLint errors: - react-hooks/set-state-in-effect: no longer calling setState in effect - @typescript-eslint/no-deprecated: replaced ConfirmModal with useConfirmModal Co-authored-by: Joel Davies --- .../BankConnection/index.native.tsx | 33 ++++++++----------- .../companyCards/BankConnection/index.tsx | 33 ++++++++----------- 2 files changed, 26 insertions(+), 40 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 87f13113ba47..8545783a4970 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -4,10 +4,10 @@ import type {WebViewNavigation} from 'react-native-webview'; import {WebView} from 'react-native-webview'; import ActivityIndicator from '@components/ActivityIndicator'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; -import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useCardFeeds from '@hooks/useCardFeeds'; +import useConfirmModal from '@hooks/useConfirmModal'; import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import useLocalize from '@hooks/useLocalize'; @@ -76,7 +76,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid; - const [isDuplicateModalVisible, setIsDuplicateModalVisible] = useState(false); + const {showConfirmModal} = useConfirmModal(); const activityReasonAttributes: SkeletonSpanReasonAttributes = { context: 'BankConnection', @@ -143,7 +143,15 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC // Handle add new card flow if (isNewFeedConnected) { if (isDuplicateFeed) { - setIsDuplicateModalVisible(true); + showConfirmModal({ + title: translate('workspace.companyCards.addNewCard.duplicateFeedModal.title'), + prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + }).then(() => { + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + }); return; } @@ -172,6 +180,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isFeedConnectionBroken, updateBrokenConnection, isNewFeedHasError, + showConfirmModal, + translate, ]); const checkIfConnectionCompleted = (navState: WebViewNavigation) => { @@ -223,23 +233,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC /> )} - { - setIsDuplicateModalVisible(false); - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - }} - onCancel={() => { - setIsDuplicateModalVisible(false); - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - }} - /> ); } diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 4d99a92c56ce..0f7c06e6e968 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -2,12 +2,12 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import ActivityIndicator from '@components/ActivityIndicator'; import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; -import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useCardFeeds from '@hooks/useCardFeeds'; +import useConfirmModal from '@hooks/useConfirmModal'; import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; @@ -80,7 +80,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const onImportPlaidAccounts = useImportPlaidAccounts(policyID); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid; - const [isDuplicateModalVisible, setIsDuplicateModalVisible] = useState(false); + const {showConfirmModal} = useConfirmModal(); const onOpenBankConnectionFlow = useCallback(() => { if (!url) { @@ -157,7 +157,15 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC customWindow?.close(); if (isDuplicateFeed) { - setIsDuplicateModalVisible(true); + showConfirmModal({ + title: translate('workspace.companyCards.addNewCard.duplicateFeedModal.title'), + prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + }).then(() => { + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + }); return; } @@ -196,6 +204,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isFeedConnectionBroken, updateBrokenConnection, isNewFeedHasError, + showConfirmModal, + translate, ]); const getContent = () => { @@ -245,23 +255,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC onBackButtonPress={handleBackButtonPress} /> {getContent()} - { - setIsDuplicateModalVisible(false); - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - }} - onCancel={() => { - setIsDuplicateModalVisible(false); - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - }} - /> ); } From 5a1e84ca6fa387b9f42ccb2e1f59af3368481268 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Thu, 21 May 2026 13:26:22 +0000 Subject: [PATCH 23/31] Fix duplicate feed modal showing on first-time Plaid additions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track whether a new feed was ever detected during the component lifecycle. After a successful first-time connection, prevFeedsData catches up to cardFeeds on subsequent renders, making newFeed undefined while plaidBankFound still matches — falsely triggering the duplicate modal. The ref prevents this by remembering that a genuine new feed was seen. Co-authored-by: Joel Davies --- .../companyCards/BankConnection/index.native.tsx | 6 +++++- src/pages/workspace/companyCards/BankConnection/index.tsx | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 8545783a4970..d57f5593b8c4 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -75,7 +75,11 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {updateBrokenConnection, isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); - const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid; + const hasEverDetectedNewFeed = useRef(false); + if (newFeed) { + hasEverDetectedNewFeed.current = true; + } + const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid && !hasEverDetectedNewFeed.current; const {showConfirmModal} = useConfirmModal(); const activityReasonAttributes: SkeletonSpanReasonAttributes = { diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 0f7c06e6e968..f6186d96dc33 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import ActivityIndicator from '@components/ActivityIndicator'; import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; @@ -79,7 +79,11 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const onImportPlaidAccounts = useImportPlaidAccounts(policyID); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); - const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid; + const hasEverDetectedNewFeed = useRef(false); + if (newFeed) { + hasEverDetectedNewFeed.current = true; + } + const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid && !hasEverDetectedNewFeed.current; const {showConfirmModal} = useConfirmModal(); const onOpenBankConnectionFlow = useCallback(() => { From b32cbf4b92f1988a5999c4c9ce60a1757f021172 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Thu, 21 May 2026 14:04:05 +0000 Subject: [PATCH 24/31] Prevent duplicate feed modal from showing twice Add a hasShownDuplicateModal ref to guard against the useEffect re-firing and calling showConfirmModal multiple times while isDuplicateFeed remains true. Co-authored-by: Joel Davies --- .../workspace/companyCards/BankConnection/index.native.tsx | 4 +++- src/pages/workspace/companyCards/BankConnection/index.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index d57f5593b8c4..beeacac7eaa7 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -76,6 +76,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); const hasEverDetectedNewFeed = useRef(false); + const hasShownDuplicateModal = useRef(false); if (newFeed) { hasEverDetectedNewFeed.current = true; } @@ -146,7 +147,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC // Handle add new card flow if (isNewFeedConnected) { - if (isDuplicateFeed) { + if (isDuplicateFeed && !hasShownDuplicateModal.current) { + hasShownDuplicateModal.current = true; showConfirmModal({ title: translate('workspace.companyCards.addNewCard.duplicateFeedModal.title'), prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index f6186d96dc33..0480c7e7108b 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -80,6 +80,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const onImportPlaidAccounts = useImportPlaidAccounts(policyID); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); const hasEverDetectedNewFeed = useRef(false); + const hasShownDuplicateModal = useRef(false); if (newFeed) { hasEverDetectedNewFeed.current = true; } @@ -160,7 +161,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC setShouldBlockWindowOpen(true); customWindow?.close(); - if (isDuplicateFeed) { + if (isDuplicateFeed && !hasShownDuplicateModal.current) { + hasShownDuplicateModal.current = true; showConfirmModal({ title: translate('workspace.companyCards.addNewCard.duplicateFeedModal.title'), prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), From 157dc459ff6bcec5ed9cbaeede495005f42d9d82 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Thu, 21 May 2026 14:48:06 +0000 Subject: [PATCH 25/31] Fix: Close RHP before showing duplicate feed modal on iOS native Navigate away from the RHP before calling showConfirmModal so the RHP is no longer visible underneath the modal on iOS native. Also moves ref access from render body into useEffect to fix react-hooks/refs lint violations. Co-authored-by: Joel Davies --- .../companyCards/BankConnection/index.native.tsx | 15 +++++++-------- .../companyCards/BankConnection/index.tsx | 15 +++++++-------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index beeacac7eaa7..fd8e620cc1c6 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -77,10 +77,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); const hasEverDetectedNewFeed = useRef(false); const hasShownDuplicateModal = useRef(false); - if (newFeed) { - hasEverDetectedNewFeed.current = true; - } - const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid && !hasEverDetectedNewFeed.current; const {showConfirmModal} = useConfirmModal(); const activityReasonAttributes: SkeletonSpanReasonAttributes = { @@ -147,16 +143,20 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC // Handle add new card flow if (isNewFeedConnected) { + // Track whether we've ever seen a new feed to distinguish duplicates + if (newFeed) { + hasEverDetectedNewFeed.current = true; + } + const isDuplicateFeed = !newFeed && isPlaid && !hasEverDetectedNewFeed.current; if (isDuplicateFeed && !hasShownDuplicateModal.current) { hasShownDuplicateModal.current = true; + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); showConfirmModal({ title: translate('workspace.companyCards.addNewCard.duplicateFeedModal.title'), prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), confirmText: translate('common.buttonConfirm'), shouldShowCancelButton: false, - }).then(() => { - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); }); return; } @@ -174,7 +174,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC } }, [ isNewFeedConnected, - isDuplicateFeed, newFeed, policyID, url, diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 0480c7e7108b..b18d4c2a992a 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -81,10 +81,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); const hasEverDetectedNewFeed = useRef(false); const hasShownDuplicateModal = useRef(false); - if (newFeed) { - hasEverDetectedNewFeed.current = true; - } - const isDuplicateFeed = isNewFeedConnected && !newFeed && isPlaid && !hasEverDetectedNewFeed.current; const {showConfirmModal} = useConfirmModal(); const onOpenBankConnectionFlow = useCallback(() => { @@ -161,16 +157,20 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC setShouldBlockWindowOpen(true); customWindow?.close(); + // Track whether we've ever seen a new feed to distinguish duplicates + if (newFeed) { + hasEverDetectedNewFeed.current = true; + } + const isDuplicateFeed = !newFeed && isPlaid && !hasEverDetectedNewFeed.current; if (isDuplicateFeed && !hasShownDuplicateModal.current) { hasShownDuplicateModal.current = true; + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); showConfirmModal({ title: translate('workspace.companyCards.addNewCard.duplicateFeedModal.title'), prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), confirmText: translate('common.buttonConfirm'), shouldShowCancelButton: false, - }).then(() => { - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); }); return; } @@ -197,7 +197,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isAllFeedsResultLoading, shouldBlockWindowOpen, isBlockedToAddNewFeeds, - isDuplicateFeed, newFeed, policyID, url, From 04576727bfe61fcfa7125165de42f3f21aeac56c Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Thu, 21 May 2026 14:53:02 +0000 Subject: [PATCH 26/31] Fix: Disable shouldHandleNavigationBack for duplicate feed modal The duplicate feed modal is shown after closeRHPFlow() and navigate() have already modified the navigation/history state. The modal's default shouldHandleNavigationBack: true causes it to push/pop browser history entries that conflict with React Navigation's history on iOS Safari, producing an error on modal dismiss. Co-authored-by: Joel Davies --- src/pages/workspace/companyCards/BankConnection/index.native.tsx | 1 + src/pages/workspace/companyCards/BankConnection/index.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index fd8e620cc1c6..2900c4f8c2fa 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -157,6 +157,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), confirmText: translate('common.buttonConfirm'), shouldShowCancelButton: false, + shouldHandleNavigationBack: false, }); return; } diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index b18d4c2a992a..bad8e1115952 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -171,6 +171,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), confirmText: translate('common.buttonConfirm'), shouldShowCancelButton: false, + shouldHandleNavigationBack: false, }); return; } From 106c54262b7355c94a074fe1f6c28a47c43d0656 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Fri, 22 May 2026 14:13:44 +0000 Subject: [PATCH 27/31] Fix: stabilize showConfirmModal and translate in useEffect deps locally Use refs to capture latest showConfirmModal and translate values, removing them from the dependency array to prevent unnecessary effect re-runs without modifying the upstream hooks. Co-authored-by: Joel Davies --- .../companyCards/BankConnection/index.native.tsx | 14 ++++++++------ .../companyCards/BankConnection/index.tsx | 14 ++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 2900c4f8c2fa..265a9f106fc5 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -78,6 +78,10 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const hasEverDetectedNewFeed = useRef(false); const hasShownDuplicateModal = useRef(false); const {showConfirmModal} = useConfirmModal(); + const showConfirmModalRef = useRef(showConfirmModal); + showConfirmModalRef.current = showConfirmModal; + const translateRef = useRef(translate); + translateRef.current = translate; const activityReasonAttributes: SkeletonSpanReasonAttributes = { context: 'BankConnection', @@ -152,10 +156,10 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC hasShownDuplicateModal.current = true; Navigation.closeRHPFlow(); Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - showConfirmModal({ - title: translate('workspace.companyCards.addNewCard.duplicateFeedModal.title'), - prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), - confirmText: translate('common.buttonConfirm'), + showConfirmModalRef.current({ + title: translateRef.current('workspace.companyCards.addNewCard.duplicateFeedModal.title'), + prompt: translateRef.current('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), + confirmText: translateRef.current('common.buttonConfirm'), shouldShowCancelButton: false, shouldHandleNavigationBack: false, }); @@ -186,8 +190,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isFeedConnectionBroken, updateBrokenConnection, isNewFeedHasError, - showConfirmModal, - translate, ]); const checkIfConnectionCompleted = (navState: WebViewNavigation) => { diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index bad8e1115952..d3de160be8e4 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -82,6 +82,10 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const hasEverDetectedNewFeed = useRef(false); const hasShownDuplicateModal = useRef(false); const {showConfirmModal} = useConfirmModal(); + const showConfirmModalRef = useRef(showConfirmModal); + showConfirmModalRef.current = showConfirmModal; + const translateRef = useRef(translate); + translateRef.current = translate; const onOpenBankConnectionFlow = useCallback(() => { if (!url) { @@ -166,10 +170,10 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC hasShownDuplicateModal.current = true; Navigation.closeRHPFlow(); Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - showConfirmModal({ - title: translate('workspace.companyCards.addNewCard.duplicateFeedModal.title'), - prompt: translate('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), - confirmText: translate('common.buttonConfirm'), + showConfirmModalRef.current({ + title: translateRef.current('workspace.companyCards.addNewCard.duplicateFeedModal.title'), + prompt: translateRef.current('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), + confirmText: translateRef.current('common.buttonConfirm'), shouldShowCancelButton: false, shouldHandleNavigationBack: false, }); @@ -210,8 +214,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isFeedConnectionBroken, updateBrokenConnection, isNewFeedHasError, - showConfirmModal, - translate, ]); const getContent = () => { From 0cda2797d9e75a2ea093ae479c73f7775c1f54c5 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Fri, 22 May 2026 14:41:33 +0000 Subject: [PATCH 28/31] Fix: Move ref updates into useEffect to fix lint and compiler errors Wraps showConfirmModalRef and translateRef updates in useEffect hooks instead of updating during render, which violates the react-hooks/refs ESLint rule and React Compiler compliance. Co-authored-by: Joel Davies --- .../companyCards/BankConnection/index.native.tsx | 10 ++++++++-- .../workspace/companyCards/BankConnection/index.tsx | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 265a9f106fc5..549861fd3627 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -79,9 +79,15 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const hasShownDuplicateModal = useRef(false); const {showConfirmModal} = useConfirmModal(); const showConfirmModalRef = useRef(showConfirmModal); - showConfirmModalRef.current = showConfirmModal; const translateRef = useRef(translate); - translateRef.current = translate; + + useEffect(() => { + showConfirmModalRef.current = showConfirmModal; + }, [showConfirmModal]); + + useEffect(() => { + translateRef.current = translate; + }, [translate]); const activityReasonAttributes: SkeletonSpanReasonAttributes = { context: 'BankConnection', diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index d3de160be8e4..3193746841b7 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -83,9 +83,15 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const hasShownDuplicateModal = useRef(false); const {showConfirmModal} = useConfirmModal(); const showConfirmModalRef = useRef(showConfirmModal); - showConfirmModalRef.current = showConfirmModal; const translateRef = useRef(translate); - translateRef.current = translate; + + useEffect(() => { + showConfirmModalRef.current = showConfirmModal; + }, [showConfirmModal]); + + useEffect(() => { + translateRef.current = translate; + }, [translate]); const onOpenBankConnectionFlow = useCallback(() => { if (!url) { From 49098bd46629a41e0f5d27cfd9e71e82b9bc2712 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Fri, 22 May 2026 15:15:00 +0000 Subject: [PATCH 29/31] Extract duplicate feed detection into useDuplicateFeedDetection hook Moves the shared duplicate-feed detection logic (refs, ref-syncing effects, and modal handling) from both index.tsx and index.native.tsx into a reusable custom hook, eliminating code duplication. Co-authored-by: Joel Davies --- .../BankConnection/index.native.tsx | 34 ++-------- .../companyCards/BankConnection/index.tsx | 36 ++--------- .../useDuplicateFeedDetection.ts | 63 +++++++++++++++++++ 3 files changed, 72 insertions(+), 61 deletions(-) create mode 100644 src/pages/workspace/companyCards/BankConnection/useDuplicateFeedDetection.ts diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 549861fd3627..3284a3db5fb0 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -7,7 +7,6 @@ import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOffli import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useCardFeeds from '@hooks/useCardFeeds'; -import useConfirmModal from '@hooks/useConfirmModal'; import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import useLocalize from '@hooks/useLocalize'; @@ -31,6 +30,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; +import useDuplicateFeedDetection from './useDuplicateFeedDetection'; type BankConnectionProps = { /** ID of the policy */ @@ -75,19 +75,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const {updateBrokenConnection, isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); - const hasEverDetectedNewFeed = useRef(false); - const hasShownDuplicateModal = useRef(false); - const {showConfirmModal} = useConfirmModal(); - const showConfirmModalRef = useRef(showConfirmModal); - const translateRef = useRef(translate); - - useEffect(() => { - showConfirmModalRef.current = showConfirmModal; - }, [showConfirmModal]); - - useEffect(() => { - translateRef.current = translate; - }, [translate]); + const {checkForDuplicateFeed} = useDuplicateFeedDetection({policyID, isPlaid}); const activityReasonAttributes: SkeletonSpanReasonAttributes = { context: 'BankConnection', @@ -153,22 +141,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC // Handle add new card flow if (isNewFeedConnected) { - // Track whether we've ever seen a new feed to distinguish duplicates - if (newFeed) { - hasEverDetectedNewFeed.current = true; - } - const isDuplicateFeed = !newFeed && isPlaid && !hasEverDetectedNewFeed.current; - if (isDuplicateFeed && !hasShownDuplicateModal.current) { - hasShownDuplicateModal.current = true; - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - showConfirmModalRef.current({ - title: translateRef.current('workspace.companyCards.addNewCard.duplicateFeedModal.title'), - prompt: translateRef.current('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), - confirmText: translateRef.current('common.buttonConfirm'), - shouldShowCancelButton: false, - shouldHandleNavigationBack: false, - }); + if (checkForDuplicateFeed(newFeed)) { return; } @@ -196,6 +169,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isFeedConnectionBroken, updateBrokenConnection, isNewFeedHasError, + checkForDuplicateFeed, ]); const checkIfConnectionCompleted = (navState: WebViewNavigation) => { diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 3193746841b7..f1588a6e6cd9 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import ActivityIndicator from '@components/ActivityIndicator'; import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; @@ -7,7 +7,6 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useCardFeeds from '@hooks/useCardFeeds'; -import useConfirmModal from '@hooks/useConfirmModal'; import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; @@ -33,6 +32,7 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; import openBankConnection from './openBankConnection'; +import useDuplicateFeedDetection from './useDuplicateFeedDetection'; let customWindow: Window | null = null; @@ -79,19 +79,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const onImportPlaidAccounts = useImportPlaidAccounts(policyID); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); - const hasEverDetectedNewFeed = useRef(false); - const hasShownDuplicateModal = useRef(false); - const {showConfirmModal} = useConfirmModal(); - const showConfirmModalRef = useRef(showConfirmModal); - const translateRef = useRef(translate); - - useEffect(() => { - showConfirmModalRef.current = showConfirmModal; - }, [showConfirmModal]); - - useEffect(() => { - translateRef.current = translate; - }, [translate]); + const {checkForDuplicateFeed} = useDuplicateFeedDetection({policyID, isPlaid}); const onOpenBankConnectionFlow = useCallback(() => { if (!url) { @@ -167,22 +155,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC setShouldBlockWindowOpen(true); customWindow?.close(); - // Track whether we've ever seen a new feed to distinguish duplicates - if (newFeed) { - hasEverDetectedNewFeed.current = true; - } - const isDuplicateFeed = !newFeed && isPlaid && !hasEverDetectedNewFeed.current; - if (isDuplicateFeed && !hasShownDuplicateModal.current) { - hasShownDuplicateModal.current = true; - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - showConfirmModalRef.current({ - title: translateRef.current('workspace.companyCards.addNewCard.duplicateFeedModal.title'), - prompt: translateRef.current('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), - confirmText: translateRef.current('common.buttonConfirm'), - shouldShowCancelButton: false, - shouldHandleNavigationBack: false, - }); + if (checkForDuplicateFeed(newFeed)) { return; } @@ -220,6 +193,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, title}: BankC isFeedConnectionBroken, updateBrokenConnection, isNewFeedHasError, + checkForDuplicateFeed, ]); const getContent = () => { diff --git a/src/pages/workspace/companyCards/BankConnection/useDuplicateFeedDetection.ts b/src/pages/workspace/companyCards/BankConnection/useDuplicateFeedDetection.ts new file mode 100644 index 000000000000..5edb5a3c0967 --- /dev/null +++ b/src/pages/workspace/companyCards/BankConnection/useDuplicateFeedDetection.ts @@ -0,0 +1,63 @@ +import {useCallback, useEffect, useRef} from 'react'; +import useConfirmModal from '@hooks/useConfirmModal'; +import useLocalize from '@hooks/useLocalize'; +import Navigation from '@libs/Navigation/Navigation'; +import ROUTES from '@src/ROUTES'; +import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; + +type UseDuplicateFeedDetectionParams = { + policyID: string | undefined; + isPlaid: boolean; +}; + +/** + * Detects and handles duplicate Plaid feed connections by showing a confirmation modal. + * Returns a function that should be called when a new feed connection is detected. + * Returns `true` if a duplicate was detected and handled, so the caller should return early. + */ +function useDuplicateFeedDetection({policyID, isPlaid}: UseDuplicateFeedDetectionParams) { + const hasEverDetectedNewFeed = useRef(false); + const hasShownDuplicateModal = useRef(false); + const {showConfirmModal} = useConfirmModal(); + const {translate} = useLocalize(); + const showConfirmModalRef = useRef(showConfirmModal); + const translateRef = useRef(translate); + + useEffect(() => { + showConfirmModalRef.current = showConfirmModal; + }, [showConfirmModal]); + + useEffect(() => { + translateRef.current = translate; + }, [translate]); + + const checkForDuplicateFeed = useCallback( + (newFeed: CompanyCardFeedWithDomainID | undefined) => { + if (newFeed) { + hasEverDetectedNewFeed.current = true; + } + + const isDuplicateFeed = !newFeed && isPlaid && !hasEverDetectedNewFeed.current; + if (isDuplicateFeed && !hasShownDuplicateModal.current) { + hasShownDuplicateModal.current = true; + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + showConfirmModalRef.current({ + title: translateRef.current('workspace.companyCards.addNewCard.duplicateFeedModal.title'), + prompt: translateRef.current('workspace.companyCards.addNewCard.duplicateFeedModal.prompt'), + confirmText: translateRef.current('common.buttonConfirm'), + shouldShowCancelButton: false, + shouldHandleNavigationBack: false, + }); + return true; + } + + return false; + }, + [isPlaid, policyID], + ); + + return {checkForDuplicateFeed}; +} + +export default useDuplicateFeedDetection; From eaa4e8802672e10cd0711beb8db26c7523fdfd39 Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Fri, 22 May 2026 15:26:18 +0000 Subject: [PATCH 30/31] Move useDuplicateFeedDetection hook to src/hooks/ Co-authored-by: Joel Davies --- .../BankConnection => hooks}/useDuplicateFeedDetection.ts | 0 .../workspace/companyCards/BankConnection/index.native.tsx | 2 +- src/pages/workspace/companyCards/BankConnection/index.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{pages/workspace/companyCards/BankConnection => hooks}/useDuplicateFeedDetection.ts (100%) diff --git a/src/pages/workspace/companyCards/BankConnection/useDuplicateFeedDetection.ts b/src/hooks/useDuplicateFeedDetection.ts similarity index 100% rename from src/pages/workspace/companyCards/BankConnection/useDuplicateFeedDetection.ts rename to src/hooks/useDuplicateFeedDetection.ts diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 3284a3db5fb0..0a357f288422 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -7,6 +7,7 @@ import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOffli import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useCardFeeds from '@hooks/useCardFeeds'; +import useDuplicateFeedDetection from '@hooks/useDuplicateFeedDetection'; import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import useLocalize from '@hooks/useLocalize'; @@ -30,7 +31,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; -import useDuplicateFeedDetection from './useDuplicateFeedDetection'; type BankConnectionProps = { /** ID of the policy */ diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index f1588a6e6cd9..b581e2a735c7 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -7,6 +7,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useCardFeeds from '@hooks/useCardFeeds'; +import useDuplicateFeedDetection from '@hooks/useDuplicateFeedDetection'; import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; @@ -32,7 +33,6 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; import openBankConnection from './openBankConnection'; -import useDuplicateFeedDetection from './useDuplicateFeedDetection'; let customWindow: Window | null = null; From 7368add3b84add65afb6ae1305fd056eb13b472c Mon Sep 17 00:00:00 2001 From: "Joel Davies (via MelvinBot)" Date: Fri, 22 May 2026 15:45:17 +0000 Subject: [PATCH 31/31] Fix: use relative imports for same-directory hooks to satisfy ESLint Co-authored-by: Joel Davies --- src/hooks/useDuplicateFeedDetection.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useDuplicateFeedDetection.ts b/src/hooks/useDuplicateFeedDetection.ts index 5edb5a3c0967..ac3bd9566b11 100644 --- a/src/hooks/useDuplicateFeedDetection.ts +++ b/src/hooks/useDuplicateFeedDetection.ts @@ -1,9 +1,9 @@ import {useCallback, useEffect, useRef} from 'react'; -import useConfirmModal from '@hooks/useConfirmModal'; -import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import ROUTES from '@src/ROUTES'; import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; +import useConfirmModal from './useConfirmModal'; +import useLocalize from './useLocalize'; type UseDuplicateFeedDetectionParams = { policyID: string | undefined;