From 04151baca1f1a89d1ca6992314b96c8dab61a7a2 Mon Sep 17 00:00:00 2001 From: littlemight Date: Thu, 30 Oct 2025 20:43:13 +0800 Subject: [PATCH] feat(i18n): extract text from admin results page --- .../IndividualResponseNavbar.tsx | 18 ++-- .../IndividualResponsePage.tsx | 44 +++++----- .../IndividualResponsePage/PaymentSection.tsx | 53 ++++++------ .../responses/ResponsesPage/ResponsesPage.tsx | 7 +- .../UnlockedResponses/DownloadButton.tsx | 26 +++--- .../ConfirmationScreen.tsx | 53 ++++-------- .../ProgressModal/CompleteScreen.tsx | 49 ++++++----- .../ResponsesTable/ResponsesTable.tsx | 86 ++++++++----------- .../common/utils/getPaymentDataView.ts | 36 +++++--- .../common/utils/mrfSubmissionView.ts | 56 ++++++++---- .../responses/individual-response/en-sg.ts | 28 ++++++ .../responses/individual-response/index.ts | 28 ++++++ .../responses/responses-page/en-sg.ts | 50 +++++++++++ .../responses/responses-page/index.ts | 41 +++++++++ 14 files changed, 369 insertions(+), 206 deletions(-) diff --git a/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponseNavbar.tsx b/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponseNavbar.tsx index f84c855d05..6314941f20 100644 --- a/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponseNavbar.tsx +++ b/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponseNavbar.tsx @@ -113,7 +113,9 @@ export const IndividualResponseNavbar = (): JSX.Element => { return `..?${searchParams}` }, [lastNavPage, lastNavSubmissionId]) - const { t } = useTranslation() + const { t } = useTranslation('translation', { + keyPrefix: 'features.adminForm.responses.individualResponse', + }) const { user } = useUser() const gb = useGrowthBook() @@ -159,20 +161,20 @@ export const IndividualResponseNavbar = (): JSX.Element => { to={backLink} > - {t('features.adminForm.responses.individualResponse.backToList')} + {t('backToList')} - {t('features.common.response')} + {t('features.common.response', { ns: 'translation', keyPrefix: '' })} {currentResponseNumber ? ` #${currentResponseNumber}` : ''} {isAdminPrintPdfEnabled && ( } isLoading={isLoading || isFormLoading} onClick={() => { @@ -201,17 +203,13 @@ export const IndividualResponseNavbar = (): JSX.Element => { isDisabled={!prevSubmissionId || isAnyFetching} onClick={handleNavigatePrev} icon={} - aria-label={t( - 'features.adminForm.responses.individualResponse.previousSubmission', - )} + aria-label={t('previousSubmission')} /> } - aria-label={t( - 'features.adminForm.responses.individualResponse.nextSubmission', - )} + aria-label={t('nextSubmission')} /> diff --git a/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponsePage.tsx b/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponsePage.tsx index 5623c79bb4..b376048ec8 100644 --- a/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponsePage.tsx +++ b/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponsePage.tsx @@ -113,7 +113,9 @@ const StackRow = ({ } export const IndividualResponsePage = (): JSX.Element => { - const { t } = useTranslation() + const { t } = useTranslation('translation', { + keyPrefix: 'features.adminForm.responses.individualResponse', + }) const { submissionId, formId } = useParams() if (!submissionId) throw new Error('Missing submissionId') if (!formId) throw new Error('Missing formId') @@ -168,12 +170,8 @@ export const IndividualResponsePage = (): JSX.Element => { return ( } - ctaText={t( - 'features.adminForm.responses.individualResponse.secretKeyVerification.ctaText', - )} - label={t( - 'features.adminForm.responses.individualResponse.secretKeyVerification.label', - )} + ctaText={t('secretKeyVerification.ctaText')} + label={t('secretKeyVerification.label')} /> ) @@ -210,15 +208,15 @@ export const IndividualResponsePage = (): JSX.Element => { > { workflowCurrentStepNumber === undefined || workflowNumTotalSteps === undefined ? '-' - : getPendingResponseAtString({ - workflowStatus, - workflowCurrentStepNumber, - workflowNumTotalSteps, - }) + : getPendingResponseAtString( + { + workflowStatus, + workflowCurrentStepNumber, + workflowNumTotalSteps, + }, + t, + ) } isLoading={isLoading} isError={isError} @@ -266,7 +267,7 @@ export const IndividualResponsePage = (): JSX.Element => { textStyle="subhead-1" py={{ base: '0', md: '0.25rem' }} > - {t('features.common.attachments')}: + {t('features.common.attachments', { ns: 'translation', keyPrefix: '' })}: @@ -292,9 +292,7 @@ export const IndividualResponsePage = (): JSX.Element => { {form?.responseMode === FormResponseMode.Multirespondent && user?.betaFlags?.mrfAdminSubmissionKey && ( { - const { t } = useTranslation() + const { t } = useTranslation('translation', { + keyPrefix: 'features.adminForm.responses.individualResponse', + }) if (!payment) return null const paymentDataMap = keyBy( - getPaymentDataView(window.location.origin, payment, formId), + getPaymentDataView(window.location.origin, payment, formId, t), 'key', ) const paymentTagProps = payment.status === PaymentStatus.Succeeded ? { - label: t('features.common.success'), + label: t('features.common.success', { ns: 'translation', keyPrefix: '' }), colorScheme: 'success', rightIcon: BiCheck, } : payment.status === PaymentStatus.PartiallyRefunded ? { - label: t( - 'features.adminForm.responses.individualResponse.paymentSection.paymentStatusLabel.partiallyRefunded', - ), + label: t('paymentSection.paymentStatusLabel.partiallyRefunded'), colorScheme: 'secondary', } : payment.status === PaymentStatus.FullyRefunded ? { - label: t( - 'features.adminForm.responses.individualResponse.paymentSection.paymentStatusLabel.fullyRefunded', - ), + label: t('paymentSection.paymentStatusLabel.fullyRefunded'), colorScheme: 'secondary', } : payment.status === PaymentStatus.Disputed ? { - label: t( - 'features.adminForm.responses.individualResponse.paymentSection.paymentStatusLabel.disputed', - ), + label: t('paymentSection.paymentStatusLabel.disputed'), colorScheme: 'warning', } : undefined // The remaining options should never appear. @@ -62,12 +58,12 @@ export const PaymentSection = ({ const payoutTagProps = payment.payoutId || payment.payoutDate ? { - label: t('features.common.success'), + label: t('features.common.success', { ns: 'translation', keyPrefix: '' }), colorScheme: 'success', rightIcon: BiCheck, } : { - label: t('features.common.pending'), + label: t('features.common.pending', { ns: 'translation', keyPrefix: '' }), colorScheme: 'secondary', } @@ -77,7 +73,10 @@ export const PaymentSection = ({ return ( - + @@ -95,7 +94,10 @@ export const PaymentSection = ({ - + @@ -118,7 +120,9 @@ function PayoutDataHeader({ colorScheme, rightIcon, }: PaymentDataHeaderProps) { - const { t } = useTranslation() + const { t } = useTranslation('translation', { + keyPrefix: 'features.adminForm.responses.individualResponse', + }) return ( @@ -127,12 +131,7 @@ function PayoutDataHeader({ {name} - + @@ -186,16 +185,16 @@ function PaymentDataItem({ isMonospace, isUrl, }: PaymentDataItemProps): JSX.Element { - const { t } = useTranslation() + const { t } = useTranslation('translation', { + keyPrefix: 'features.adminForm.responses.individualResponse', + }) return ( {name}: {isUrl ? ( - {t( - 'features.adminForm.responses.individualResponse.paymentSection.paymentDataItemPdfDownloadLabel', - )} + {t('paymentSection.paymentDataItemPdfDownloadLabel')} ) : ( value diff --git a/frontend/src/features/admin-form/responses/ResponsesPage/ResponsesPage.tsx b/frontend/src/features/admin-form/responses/ResponsesPage/ResponsesPage.tsx index 1f89a76dd2..5cbec42e2f 100644 --- a/frontend/src/features/admin-form/responses/ResponsesPage/ResponsesPage.tsx +++ b/frontend/src/features/admin-form/responses/ResponsesPage/ResponsesPage.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { FormResponseMode } from '~shared/types/form' import { useToast } from '~hooks/useToast' @@ -9,6 +10,9 @@ import { ResponsesPageSkeleton } from './ResponsesPageSkeleton' import { StorageResponsesTab } from './storage' export const ResponsesPage = (): JSX.Element => { + const { t } = useTranslation('translation', { + keyPrefix: 'features.adminForm.responses.responsesPage', + }) const { data: form, isLoading } = useAdminForm() const toast = useToast({ status: 'danger' }) @@ -17,8 +21,7 @@ export const ResponsesPage = (): JSX.Element => { if (!form) { toast({ - description: - 'There was an error retrieving your form. Please try again later.', + description: t('errors.formRetrievalError'), }) return } diff --git a/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/DownloadButton.tsx b/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/DownloadButton.tsx index fd8d66299e..564997fe00 100644 --- a/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/DownloadButton.tsx +++ b/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/DownloadButton.tsx @@ -2,7 +2,6 @@ import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useThrottle } from 'react-use' import { Box, MenuButton, Text, useDisclosure } from '@chakra-ui/react' -import simplur from 'simplur' import { BxsChevronDown } from '~assets/icons/BxsChevronDown' import { BxsChevronUp } from '~assets/icons/BxsChevronUp' @@ -71,29 +70,34 @@ export const DownloadButton = (): JSX.Element => { onSuccess: ({ successCount, expectedCount, errorCount }) => { if (downloadParams?.responsesCount === 0) { toast({ - description: 'No responses to download', + description: t('storage.unlockedResponses.downloadButton.toasts.noResponses'), }) return } if (errorCount > 0) { toast({ status: 'warning', - description: simplur`Partial success. ${successCount}/${expectedCount} ${[ + description: t('storage.unlockedResponses.downloadButton.toasts.partialSuccess', { successCount, - ]}response[|s] [was|were] decrypted. ${errorCount} failed.`, + expectedCount, + count: successCount, + errorCount, + }), }) return } toast({ - description: simplur`Success. ${successCount}/${expectedCount} ${[ + description: t('storage.unlockedResponses.downloadButton.toasts.success', { successCount, - ]}response[|s] [was|were] decrypted.`, + expectedCount, + count: successCount, + }), }) }, onError: () => { toast({ status: 'danger', - description: 'Failed to start download. Please try again later.', + description: t('storage.unlockedResponses.downloadButton.toasts.failedToStart'), }) }, onSettled: (decryptResult) => { @@ -138,17 +142,19 @@ export const DownloadButton = (): JSX.Element => { handleModalClose() toast({ status: 'warning', - description: 'Responses download has been canceled.', + description: t('storage.unlockedResponses.downloadButton.toasts.downloadCanceled'), }) setDownloadMetadata({ isCanceled: true }) - }, [handleModalClose, toast]) + }, [handleModalClose, t, toast]) const handleAttachmentsDownloadCancel = useCallback(() => { resetDownload() setDownloadMetadata({ isCanceled: true }) }, [resetDownload]) - const { t } = useTranslation() + const { t } = useTranslation('translation', { + keyPrefix: 'features.adminForm.responses.responsesPage', + }) return ( <> diff --git a/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/DownloadWithAttachmentModal/ConfirmationScreen.tsx b/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/DownloadWithAttachmentModal/ConfirmationScreen.tsx index 0506982bcc..4a15e74168 100644 --- a/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/DownloadWithAttachmentModal/ConfirmationScreen.tsx +++ b/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/DownloadWithAttachmentModal/ConfirmationScreen.tsx @@ -1,4 +1,4 @@ -import { useTranslation } from 'react-i18next' +import { Trans, useTranslation } from 'react-i18next' import { BiCheck } from 'react-icons/bi' import { Badge, @@ -46,7 +46,9 @@ export const ConfirmationScreen = ({ responsesCount, }: ConfirmationScreenProps): JSX.Element => { const isMobile = useIsMobile() - const { t } = useTranslation() + const { t } = useTranslation('translation', { + keyPrefix: 'features.adminForm.responses.responsesPage', + }) return ( <> @@ -54,9 +56,7 @@ export const ConfirmationScreen = ({ - {t( - 'features.adminForm.responses.responsesPage.storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.title', - )} + {t('storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.title')} {t('features.common.betaBadgeLabel')} @@ -66,59 +66,46 @@ export const ConfirmationScreen = ({ - Separate zip files will be downloaded, one for each response. - You can adjust the date range before proceeding. + }} + />

- {t( - 'features.adminForm.responses.responsesPage.storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.numberOfResponsesAndAttachments', - )} + {t('storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.numberOfResponsesAndAttachments')} : {' '} {responsesCount.toLocaleString()}
- {t( - 'features.adminForm.responses.responsesPage.storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.estimatedTime', - )} + {t('storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.estimatedTime')} : {' '} - {t( - 'features.adminForm.responses.responsesPage.storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.estimatedTimeReference', - )} + {t('storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.estimatedTimeReference')}
- {t( - 'features.adminForm.responses.responsesPage.storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.intensiveOperationWarning.title', - )} + {t('storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.intensiveOperationWarning.title')} - {t( - 'features.adminForm.responses.responsesPage.storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.intensiveOperationWarning.doNotUseIE', - )} + {t('storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.intensiveOperationWarning.doNotUseIE')} - {t( - 'features.adminForm.responses.responsesPage.storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.intensiveOperationWarning.ensureStrongNetworkConnectivity', - )} + {t('storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.intensiveOperationWarning.ensureStrongNetworkConnectivity')} - {t( - 'features.adminForm.responses.responsesPage.storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.intensiveOperationWarning.ensureEnoughDiskSpace', - )} + {t('storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.intensiveOperationWarning.ensureEnoughDiskSpace')} {responsesCount === 0 && ( - {t( - 'features.adminForm.responses.responsesPage.storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.noResponsesInSelectedDateRange', - )} + {t('storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.noResponsesInSelectedDateRange')} )}
@@ -135,9 +122,7 @@ export const ConfirmationScreen = ({ isLoading={isDownloading} isDisabled={responsesCount === 0} > - {t( - 'features.adminForm.responses.responsesPage.storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.startDownload', - )} + {t('storage.unlockedResponses.downloadWithAttachmentModal.confirmationScreen.startDownload')} diff --git a/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/ResponsesTable/ResponsesTable.tsx b/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/ResponsesTable/ResponsesTable.tsx index b8a2a50138..78bd9a5ce9 100644 --- a/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/ResponsesTable/ResponsesTable.tsx +++ b/frontend/src/features/admin-form/responses/ResponsesPage/storage/UnlockedResponses/ResponsesTable/ResponsesTable.tsx @@ -30,12 +30,6 @@ import Badge from '~components/Badge' import { useAdminForm } from '~features/admin-form/common/queries' import { getPendingResponseAtString } from '~features/admin-form/responses/common/utils/mrfSubmissionView' -import { - MRF_PENDING_RESPONSE_AT_LABEL, - MRF_REMINDERS_LABEL, - MRF_RESPONSE_TIMESTAMP_LABEL, - MRF_WORKFLOW_STATUS_LABEL, -} from '~features/admin-form/responses/constants' import { useUnlockedResponses } from '../UnlockedResponsesProvider' @@ -75,6 +69,13 @@ function PendingBadge() { ) } +function PayoutPendingText() { + const { t } = useTranslation('translation', { + keyPrefix: 'features.adminForm.responses.responsesPage', + }) + return t('storage.unlockedResponses.responsesTable.status.payoutPending') +} + function CompletedBadge() { const { t } = useTranslation() return ( @@ -108,23 +109,24 @@ function NotApprovedBadge() { ) } -const BASE_RESPONSE_TABLE_COLUMNS: Column[] = [ +// Helper function to create base columns with translations +const createBaseColumns = (t: (key: string) => string): Column[] => [ { - Header: '#', + Header: t('storage.unlockedResponses.responsesTable.headers.number'), accessor: 'number', - width: 80, // width is used for both the flex-basis and flex-grow - minWidth: 80, // minWidth is only used as a limit for resizing - maxWidth: 100, // maxWidth is only used as a limit for resizing + width: 80, + minWidth: 80, + maxWidth: 100, }, { - Header: 'Response ID', + Header: t('storage.unlockedResponses.responsesTable.headers.responseId'), accessor: 'refNo', width: 300, minWidth: 300, maxWidth: 300, }, { - Header: 'Timestamp', + Header: t('storage.unlockedResponses.responsesTable.headers.timestamp'), accessor: 'submissionTime', width: 250, minWidth: 250, @@ -132,9 +134,10 @@ const BASE_RESPONSE_TABLE_COLUMNS: Column[] = [ }, ] -const PAYMENT_COLUMNS: Column[] = [ +// Helper function to create payment columns with translations +const createPaymentColumns = (t: (key: string) => string): Column[] => [ { - Header: 'Email', + Header: t('storage.unlockedResponses.responsesTable.headers.email'), accessor: ({ payments }) => { if (!payments?.email) { return '' @@ -144,9 +147,8 @@ const PAYMENT_COLUMNS: Column[] = [ minWidth: 250, width: 250, }, - { - Header: 'Paid Amount (S$)', // (amt responder paid) + Header: t('storage.unlockedResponses.responsesTable.headers.paidAmount'), accessor: ({ payments }) => { if (!payments) { return '' @@ -156,9 +158,8 @@ const PAYMENT_COLUMNS: Column[] = [ minWidth: 150, width: 150, }, - { - Header: 'Fees (S$)', // (paid - net) + Header: t('storage.unlockedResponses.responsesTable.headers.fees'), accessor: ({ payments }) => { if (!payments?.transactionFee) { return '' @@ -166,25 +167,22 @@ const PAYMENT_COLUMNS: Column[] = [ if (payments.transactionFee < 0) { return '' } - return `${centsToDollars(payments.transactionFee)}` }, minWidth: 150, width: 150, }, - { - Header: 'Net Amount (S$)', // (amt they receive in bank) + Header: t('storage.unlockedResponses.responsesTable.headers.netAmount'), accessor: ({ payments }) => getNetAmount(payments), minWidth: 150, width: 150, }, - { - Header: 'Payout Date', + Header: t('storage.unlockedResponses.responsesTable.headers.payoutDate'), accessor: ({ payments }) => { if (!payments) { - return 'Pending' + return } return payments.payoutDate }, @@ -194,23 +192,24 @@ const PAYMENT_COLUMNS: Column[] = [ }, ] -const MRF_RESPONSE_TABLE_COLUMNS: Column[] = [ +// Helper function to create MRF columns with translations +const createMrfColumns = (t: (key: string) => string): Column[] => [ { - Header: '#', + Header: t('storage.unlockedResponses.responsesTable.headers.number'), accessor: 'number', width: 80, minWidth: 80, maxWidth: 100, }, { - Header: 'Response ID', + Header: t('storage.unlockedResponses.responsesTable.headers.responseId'), accessor: 'refNo', width: 240, minWidth: 240, maxWidth: 240, }, { - Header: MRF_WORKFLOW_STATUS_LABEL, + Header: t('storage.unlockedResponses.responsesTable.mrf.workflowStatus'), accessor: ({ mrf }) => { if (!mrf?.workflowStatus) { return '' @@ -233,7 +232,7 @@ const MRF_RESPONSE_TABLE_COLUMNS: Column[] = [ maxWidth: 160, }, { - Header: MRF_PENDING_RESPONSE_AT_LABEL, + Header: t('storage.unlockedResponses.responsesTable.mrf.pendingResponseAt'), accessor: ({ mrf }) => { const workflowStatus = mrf?.workflowStatus const workflowCurrentStepNumber = mrf?.workflowCurrentStepNumber @@ -256,23 +255,14 @@ const MRF_RESPONSE_TABLE_COLUMNS: Column[] = [ maxWidth: 180, }, { - Header: MRF_RESPONSE_TIMESTAMP_LABEL, + Header: t('storage.unlockedResponses.responsesTable.mrf.responseTimestamp'), accessor: 'submissionTime', - // TODO(FRM-1933): using submissionTime as we are undecided on showing first submission vs lastSubmittedAt - // accessor: ({ mrf }) => - // mrf?.lastSubmittedAt - // ? formatInTimeZone( - // mrf.lastSubmittedAt, - // 'Asia/Singapore', - // 'do MMM yyyy, hh:mm:ss a', - // ) - // : '', width: 240, minWidth: 240, maxWidth: 240, }, { - Header: MRF_REMINDERS_LABEL, + Header: t('storage.unlockedResponses.responsesTable.mrf.reminders'), Cell: ({ row }) => { const isPending = row.original.mrf?.workflowStatus === WorkflowStatus.PENDING @@ -288,10 +278,10 @@ const MRF_RESPONSE_TABLE_COLUMNS: Column[] = [ }, ] -const PAYMENT_RESPONSE_TABLE_COLUMNS = - BASE_RESPONSE_TABLE_COLUMNS.concat(PAYMENT_COLUMNS) - export const ResponsesTable = () => { + const { t } = useTranslation('translation', { + keyPrefix: 'features.adminForm.responses.responsesPage', + }) const { data: form } = useAdminForm() const isPaymentsForm = form?.responseMode === FormResponseMode.Encrypt @@ -325,13 +315,13 @@ export const ResponsesTable = () => { const columns = useMemo(() => { if (isMultiRespondentForm) { - return MRF_RESPONSE_TABLE_COLUMNS + return createMrfColumns(t) } if (isPaymentsForm) { - return PAYMENT_RESPONSE_TABLE_COLUMNS + return createBaseColumns(t).concat(createPaymentColumns(t)) } - return BASE_RESPONSE_TABLE_COLUMNS - }, [isMultiRespondentForm, isPaymentsForm]) + return createBaseColumns(t) + }, [isMultiRespondentForm, isPaymentsForm, t]) const { prepareRow, diff --git a/frontend/src/features/admin-form/responses/common/utils/getPaymentDataView.ts b/frontend/src/features/admin-form/responses/common/utils/getPaymentDataView.ts index 5d2bd46080..a090da2fab 100644 --- a/frontend/src/features/admin-form/responses/common/utils/getPaymentDataView.ts +++ b/frontend/src/features/admin-form/responses/common/utils/getPaymentDataView.ts @@ -1,3 +1,5 @@ +import { TFunction } from 'i18next' + import { SubmissionPaymentDto } from '~shared/types' import { getPaymentInvoiceDownloadUrl } from '~features/public-form/utils/urls' @@ -36,42 +38,52 @@ const getFullInvoiceDownloadUrl = ( /** * Helper function to obtain the payment field data that we want to display to * admins, used both in the individual response page and the exported CSV. + * @param hostOrigin the origin of the host * @param payment the payment data object returned within the submission object + * @param formId the form ID + * @param t optional translation function. If not provided, uses English defaults * @returns payment data view with an array of names and values, ordered in CSV column order. */ export const getPaymentDataView = ( hostOrigin: string, payment: SubmissionPaymentDto, formId: string, + t?: TFunction, ): PaymentDataViewItem[] => // Payment data association of keys to values, in CSV column order [ { key: 'status', - name: 'Payment status', + name: t ? t('paymentDataView.fields.paymentStatus') : 'Payment status', value: toSentenceCase(payment.status), }, - { key: 'email', name: 'Payer', value: payment.email }, + { + key: 'email', + name: t ? t('paymentDataView.fields.payer') : 'Payer', + value: payment.email, + }, { key: 'receiptUrl', - name: 'Proof of Payment', + name: t ? t('paymentDataView.fields.proofOfPayment') : 'Proof of Payment', value: getFullInvoiceDownloadUrl(hostOrigin, formId, payment.id), }, { key: 'paymentIntentId', - name: 'Payment intent ID', + name: t + ? t('paymentDataView.fields.paymentIntentId') + : 'Payment intent ID', value: payment.paymentIntentId, }, { key: 'amount', - name: 'Payment amount', + name: t ? t('paymentDataView.fields.paymentAmount') : 'Payment amount', value: centsToDollarString(payment.amount), }, { key: 'products', - name: 'Product/service', + name: t ? t('paymentDataView.fields.productService') : 'Product/service', value: payment.products ?.map(({ name, quantity }) => `${name} x ${quantity}`) @@ -79,24 +91,28 @@ export const getPaymentDataView = ( }, { key: 'paymentDate', - name: 'Payment date and time', + name: t + ? t('paymentDataView.fields.paymentDateTime') + : 'Payment date and time', value: payment.paymentDate, }, { key: 'transactionFee', - name: 'Transaction fee', + name: t ? t('paymentDataView.fields.transactionFee') : 'Transaction fee', value: centsToDollarString(payment.transactionFee), }, { key: 'payoutId', - name: 'Payout ID', + name: t ? t('paymentDataView.fields.payoutId') : 'Payout ID', value: payment.payoutId ?? '-', }, { key: 'payoutDate', - name: 'Payout date and time', + name: t + ? t('paymentDataView.fields.payoutDateTime') + : 'Payout date and time', value: payment.payoutDate ?? '-', }, ] diff --git a/frontend/src/features/admin-form/responses/common/utils/mrfSubmissionView.ts b/frontend/src/features/admin-form/responses/common/utils/mrfSubmissionView.ts index e4896958b3..eb72a99d91 100644 --- a/frontend/src/features/admin-form/responses/common/utils/mrfSubmissionView.ts +++ b/frontend/src/features/admin-form/responses/common/utils/mrfSubmissionView.ts @@ -1,3 +1,5 @@ +import { TFunction } from 'i18next' + import { StrippedFormWorkflowDto, SubmittedStep, @@ -18,11 +20,14 @@ interface CurrentWorkflowInfo { } /** Gets the business friendly message for the current pending step of the workflow. */ -export const getPendingResponseAtString = ({ - workflowStatus, - workflowCurrentStepNumber, - workflowNumTotalSteps, -}: CurrentWorkflowInfo) => { +export const getPendingResponseAtString = ( + { + workflowStatus, + workflowCurrentStepNumber, + workflowNumTotalSteps, + }: CurrentWorkflowInfo, + t?: TFunction, +) => { const isPending = workflowStatus === WorkflowStatus.PENDING && workflowCurrentStepNumber < workflowNumTotalSteps @@ -34,23 +39,36 @@ export const getPendingResponseAtString = ({ // Thus, it should mean that it is pending the response of N+1 const pendingStep = workflowCurrentStepNumber + 1 - return getCurrentStepString({ - workflowCurrentStepNumber: pendingStep, - workflowNumTotalSteps, - }) + return getCurrentStepString( + { + workflowCurrentStepNumber: pendingStep, + workflowNumTotalSteps, + }, + t, + ) } /** Gets the business friendly string for current step of workflow. */ -const getCurrentStepString = ({ - workflowCurrentStepNumber, - workflowNumTotalSteps, -}: Pick< - CurrentWorkflowInfo, - 'workflowNumTotalSteps' | 'workflowCurrentStepNumber' ->) => { - return workflowCurrentStepNumber && workflowNumTotalSteps - ? `Step ${workflowCurrentStepNumber} of ${workflowNumTotalSteps}` - : '' +const getCurrentStepString = ( + { + workflowCurrentStepNumber, + workflowNumTotalSteps, + }: Pick< + CurrentWorkflowInfo, + 'workflowNumTotalSteps' | 'workflowCurrentStepNumber' + >, + t?: TFunction, +) => { + if (!workflowCurrentStepNumber || !workflowNumTotalSteps) { + return '' + } + + return t + ? t('mrf.workflowStep', { + currentStep: workflowCurrentStepNumber, + totalSteps: workflowNumTotalSteps, + }) + : `Step ${workflowCurrentStepNumber} of ${workflowNumTotalSteps}` } /** Gets the business friendly string for MRF submission status. */ diff --git a/frontend/src/i18n/locales/features/admin-form/responses/individual-response/en-sg.ts b/frontend/src/i18n/locales/features/admin-form/responses/individual-response/en-sg.ts index df5f23f7ea..7390d850fd 100644 --- a/frontend/src/i18n/locales/features/admin-form/responses/individual-response/en-sg.ts +++ b/frontend/src/i18n/locales/features/admin-form/responses/individual-response/en-sg.ts @@ -11,7 +11,15 @@ export const enSG: ResponsesIndividualResponse = { downloadAttachmentsAsZip: 'Download {attachmentSize, plural, =1 {# attachment} other {# attachments}} as .zip', responseLinkLabel: 'Response link', + labels: { + responseId: 'Response ID', + timestamp: 'Timestamp', + }, paymentSection: { + headers: { + payment: 'Payment', + payout: 'Payout to bank account', + }, paymentStatusLabel: { partiallyRefunded: 'Partially refunded', fullyRefunded: 'Fully refunded', @@ -21,6 +29,26 @@ export const enSG: ResponsesIndividualResponse = { Depending on payment method, payouts happen 1 - 3 working days after a respondent makes payment.`, paymentDataItemPdfDownloadLabel: 'Download as PDF', }, + paymentDataView: { + fields: { + paymentStatus: 'Payment status', + payer: 'Payer', + proofOfPayment: 'Proof of Payment', + paymentIntentId: 'Payment intent ID', + paymentAmount: 'Payment amount', + productService: 'Product/service', + paymentDateTime: 'Payment date and time', + transactionFee: 'Transaction fee', + payoutId: 'Payout ID', + payoutDateTime: 'Payout date and time', + }, + }, + mrf: { + workflowStep: 'Step {currentStep} of {totalSteps}', + }, + individualResponseNavbar: { + printAriaLabel: 'Print', + }, decryptedAttachment: { aria: 'Download file', }, diff --git a/frontend/src/i18n/locales/features/admin-form/responses/individual-response/index.ts b/frontend/src/i18n/locales/features/admin-form/responses/individual-response/index.ts index c041a9be74..20024b1b85 100644 --- a/frontend/src/i18n/locales/features/admin-form/responses/individual-response/index.ts +++ b/frontend/src/i18n/locales/features/admin-form/responses/individual-response/index.ts @@ -10,7 +10,15 @@ export interface ResponsesIndividualResponse { } downloadAttachmentsAsZip: string responseLinkLabel: string + labels: { + responseId: string + timestamp: string + } paymentSection: { + headers: { + payment: string + payout: string + } paymentStatusLabel: { partiallyRefunded: string fullyRefunded: string @@ -19,6 +27,26 @@ export interface ResponsesIndividualResponse { tooltipLabel: string paymentDataItemPdfDownloadLabel: string } + paymentDataView: { + fields: { + paymentStatus: string + payer: string + proofOfPayment: string + paymentIntentId: string + paymentAmount: string + productService: string + paymentDateTime: string + transactionFee: string + payoutId: string + payoutDateTime: string + } + } + mrf: { + workflowStep: string + } + individualResponseNavbar: { + printAriaLabel: string + } decryptedAttachment: { aria: string } diff --git a/frontend/src/i18n/locales/features/admin-form/responses/responses-page/en-sg.ts b/frontend/src/i18n/locales/features/admin-form/responses/responses-page/en-sg.ts index 0426686968..3f3d240694 100644 --- a/frontend/src/i18n/locales/features/admin-form/responses/responses-page/en-sg.ts +++ b/frontend/src/i18n/locales/features/admin-form/responses/responses-page/en-sg.ts @@ -1,6 +1,10 @@ import { ResponsesResponsesPage } from '.' export const enSG: ResponsesResponsesPage = { + errors: { + formRetrievalError: + 'There was an error retrieving your form. Please try again later.', + }, emptyResponses: { title: "You don't have any responses yet.", subtitle: 'Try using {link} to send out your form links!', @@ -22,6 +26,8 @@ export const enSG: ResponsesResponsesPage = { }, confirmationScreen: { title: 'Download responses and attachments', + description: + 'Separate zip files will be downloaded, one for each response. You can adjust the date range before proceeding.', numberOfResponsesAndAttachments: 'Number of responses and attachments', estimatedTime: 'Estimated time', @@ -48,6 +54,21 @@ export const enSG: ResponsesResponsesPage = { completeScreen: { downloadComplete: 'Download complete', backToResponses: 'Back to responses', + successMessages: { + allResponses: 'All responses have been downloaded successfully.', + allResponsesWithAttachments: + 'All responses and attachments have been downloaded successfully.', + partialSuccessWithAttachments: + '**{successCount}** {count, plural, =1 {response and attachment has} other {responses and attachments have}} been downloaded successfully, refer to the downloaded CSV file for more details', + partialSuccess: + '**{successCount}** {count, plural, =1 {response has} other {responses have}} been downloaded successfully, refer to the downloaded CSV file for more details', + }, + errorMessages: { + withAttachments: + '**{errorCount}** {count, plural, =1 {response and attachment} other {responses and attachments}} could not be downloaded.', + withoutAttachments: + '**{errorCount}** {count, plural, =1 {response} other {responses}} could not be downloaded.', + }, }, content: { title: 'Downloading...', @@ -56,6 +77,26 @@ export const enSG: ResponsesResponsesPage = { }, }, responsesTable: { + headers: { + number: '#', + responseId: 'Response ID', + timestamp: 'Timestamp', + email: 'Email', + paidAmount: 'Paid Amount (S$)', + fees: 'Fees (S$)', + netAmount: 'Net Amount (S$)', + payoutDate: 'Payout Date', + }, + status: { + payoutPending: 'Pending', + }, + mrf: { + responseTimestamp: 'Response timestamp', + pendingResponseAt: 'Pending response at', + workflowStatus: 'Workflow status', + reminders: 'Reminders', + statusTrackingLink: 'Status tracking link', + }, sendReminderButton: { sendReminder: 'Send reminder', reminderSent: 'Reminder sent', @@ -76,6 +117,15 @@ export const enSG: ResponsesResponsesPage = { csvOnly: 'CSV only', csvWithAttachments: 'CSV with attachments', }, + toasts: { + noResponses: 'No responses to download', + partialSuccess: + 'Partial success. {successCount}/{expectedCount} {count, plural, =1 {response was} other {responses were}} decrypted. {errorCount} failed.', + success: + 'Success. {successCount}/{expectedCount} {count, plural, =1 {response was} other {responses were}} decrypted.', + failedToStart: 'Failed to start download. Please try again later.', + downloadCanceled: 'Responses download has been canceled.', + }, }, unlockedResponses: { resultsFound: '{count, plural, =1 {result} other {results}} found', diff --git a/frontend/src/i18n/locales/features/admin-form/responses/responses-page/index.ts b/frontend/src/i18n/locales/features/admin-form/responses/responses-page/index.ts index b3057f7f9c..aa11d9bd65 100644 --- a/frontend/src/i18n/locales/features/admin-form/responses/responses-page/index.ts +++ b/frontend/src/i18n/locales/features/admin-form/responses/responses-page/index.ts @@ -1,6 +1,9 @@ export * from './en-sg' export interface ResponsesResponsesPage { + errors: { + formRetrievalError: string + } emptyResponses: { title: string subtitle: string @@ -19,6 +22,7 @@ export interface ResponsesResponsesPage { } confirmationScreen: { title: string + description: string numberOfResponsesAndAttachments: string estimatedTime: string estimatedTimeReference: string @@ -39,6 +43,16 @@ export interface ResponsesResponsesPage { completeScreen: { downloadComplete: string backToResponses: string + successMessages: { + allResponses: string + allResponsesWithAttachments: string + partialSuccessWithAttachments: string + partialSuccess: string + } + errorMessages: { + withAttachments: string + withoutAttachments: string + } } content: { title: string @@ -47,6 +61,26 @@ export interface ResponsesResponsesPage { } } responsesTable: { + headers: { + number: string + responseId: string + timestamp: string + email: string + paidAmount: string + fees: string + netAmount: string + payoutDate: string + } + status: { + payoutPending: string + } + mrf: { + responseTimestamp: string + pendingResponseAt: string + workflowStatus: string + reminders: string + statusTrackingLink: string + } sendReminderButton: { sendReminder: string reminderSent: string @@ -65,6 +99,13 @@ export interface ResponsesResponsesPage { csvOnly: string csvWithAttachments: string } + toasts: { + noResponses: string + partialSuccess: string + success: string + failedToStart: string + downloadCanceled: string + } } unlockedResponses: { resultsFound: string