Skip to content

[Due for payment 2026-09-28] Rename "Download as PDF" to "Download report" and add singular/plural support #101055

Closed

Description

@mollfpr

Problem

Per the naming spec in Expensify/Expensify#653740, the Download report label should read:

  • Individual report page: always singular Download report (the page only ever acts on one report).
  • Spend/Reports/Expenses page (bulk actions): singular Download report when exactly one report is selected, plural Download reports when more than one is selected.

Today both places use common.downloadAsPDF ("Download as PDF") with no singular/plural handling:

  • src/hooks/useExportActions.ts (individual report)
  • src/hooks/useSearchBulkActions.ts (bulk actions)

PR #96947 added Download receipts to the report page but left Download as PDF unchanged, since renaming it was out of scope for #95356.

Solution

  • Add new translation keys, across all languages: common.downloadReport (singular), common.downloadReports (plural).
  • In src/hooks/useExportActions.ts, change the DOWNLOAD_PDF action's text from common.downloadAsPDF to common.downloadReport (always singular).
  • In src/hooks/useSearchBulkActions.ts, change the DOWNLOAD_PDF option's text to pick common.downloadReport or common.downloadReports based on selectedReportIDs.length.
Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

  1. self-assigned this
    on Sep 14, 2026
  2. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Sep 14, 2026
  3. melvin-bot commented on Sep 14, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @abdulrahuman5196 (External)

  4. melvin-bot commented on Sep 14, 2026

    @melvin-bot

    Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.

  5. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on Sep 14, 2026
  6. melvin-bot commented on Sep 14, 2026

    @melvin-bot

    Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.

  7. Dip-G-Git commented on Sep 14, 2026

    @Dip-G-Git
  8. github-actions commented on Sep 14, 2026

    @github-actions
    Contributor

    ⚠️ @Dip-G-Git Thanks for your interest. To be considered for this job, please post a proposal following the proposal template (note the mandatory sections). Comments claiming the job without one are not reviewed.

  9. wildan-m commented on Sep 14, 2026

    @wildan-m
    Contributor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    The "Download report" action is currently labelled "Download as PDF" in both places it appears, and it has no singular/plural handling. Per the naming spec, the individual report page should always read "Download report" (it only ever acts on one report), while the Spend/Reports/Expenses bulk-action menu should read "Download report" when exactly one report is selected and "Download reports" when more than one is selected.

    What is the root cause of that problem?

    Both call sites read the same single, fixed translation key common.downloadAsPDF, which resolves to the literal string "Download as PDF". There is no separate singular/plural key and no count-based selection anywhere in the path.

    On the individual report page, the DOWNLOAD_PDF secondary action hardcodes that key:

    [CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF]: {
    value: CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF,
    text: translate('common.downloadAsPDF'),
    icon: expensifyIcons.Download,
    sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.DOWNLOAD_PDF,

    In the bulk-action menu, the DOWNLOAD_PDF option uses the same key, even though the count of selected reports is already available in scope as selectedReportIDs.length (the very next lines branch on selectedReportIDs.length === 1 for the download behaviour itself):

    if (isExpenseReportSearch && selectedReportIDs.length > 0) {
    options.push({
    icon: expensifyIcons.Download,
    text: translate('common.downloadAsPDF'),
    value: CONST.SEARCH.BULK_ACTION_TYPES.DOWNLOAD_PDF,
    shouldCloseModalOnSelect: true,
    onSelected: async () => {
    if (isOffline) {
    setIsOfflineModalVisible(true);
    return;
    }
    if (selectedReportIDs.length === 1) {
    const reportIDForPDF = selectedReportIDs.at(0);

    And the key itself is defined once per language with the old PDF-centric wording:

    downloadAsPDF: 'Download as PDF',

    So the label is wrong in wording and cannot vary by count, because the only thing driving it is a constant key.

    What changes do you think we should make in order to solve the problem?

    1. Add two new keys to the common section of every language file (en, es, de, el, fr, it, ja, nl, pl, pt-BR, zh-hans): downloadReport for the singular label and downloadReports for the plural one. In English these read "Download report" and "Download reports"; the other languages get their correctly pluralised equivalents. Placing them in common keeps them next to the existing downloadAsPDF entry and available to both hooks without any new import.

    2. In useExportActions.ts, change the text of the DOWNLOAD_PDF secondary action from the old key to the new singular key. This page always operates on exactly one report, so it is a plain constant swap with no count logic.

    3. In useSearchBulkActions.ts, change the text of the DOWNLOAD_PDF bulk option so it selects between the plural and singular key based on whether selectedReportIDs.length is greater than one. That value is already in scope and already used a few lines below to choose between the single-report and multi-report download call, so the label now derives from exactly the same source of truth as the behaviour, which keeps the two from drifting apart. The option is only pushed when selectedReportIDs.length > 0, so the count is always at least one and no empty-state wording is needed.

    4. Remove the now-unused downloadAsPDF key from every language file. After the two swaps above, it has no remaining consumer anywhere in src outside the language definitions themselves, so leaving it in would be dead translation copy that future contributors could reintroduce by mistake.

    The result is that the individual report page reads "Download report" unconditionally, and the bulk menu reads "Download report" for a single selection and "Download reports" for two or more, with all languages covered.

    Compare branch: https://github.com/Expensify/App/compare/main...wildan-m:App:wildan/101055-download-report-label?expand=1

    What alternative solutions did you explore? (Optional)

    An alternative is a single count-parameterised key, for example common.downloadReport taking a count parameter and resolving the plural form inside the translation function. That is more flexible for languages with several plural forms, but it is heavier than this issue needs: only two forms are ever required here, the count is already available at both call sites, and the parameterised variant would add a params type entry plus a function-valued translation in all eleven language files for no behavioural gain. I therefore kept the two flat keys, matching how similar singular/plural label pairs are already handled elsewhere in the codebase.

  10. github-actions commented on Sep 14, 2026

    @github-actions
    Contributor

    🤖 ProposalPolice™ is tracking duplicate proposals for this issue using an OpenAI Conversation. This comment stores that Conversation's ID and can be safely ignored.

  11. abbasifaizan70 commented on Sep 14, 2026

    @abbasifaizan70
    Contributor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    Per the naming spec in Expensify/Expensify#653740, the PDF download action should no longer read "Download as PDF". Instead it should read:

    • Individual report page — always the singular Download report (the page only ever exports one report).
    • Spend / Reports / Expenses page (bulk actions) — Download report (singular) when exactly one report is selected, and Download reports (plural) when more than one is selected.

    Today both entry points render the static common.downloadAsPDF ("Download as PDF") label with no singular/plural handling.

    What is the root cause of that problem?

    There is no bug in behavior — the label was simply never updated after the naming spec, and neither call site has singular/plural handling. Two places hardcode common.downloadAsPDF:

    1. The individual report page's DOWNLOAD_PDF secondary action sets its text to translate('common.downloadAsPDF'). It always acts on a single report, so it should always read the singular Download report.

    [CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF]: {
    value: CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF,
    text: translate('common.downloadAsPDF'),
    icon: expensifyIcons.Download,
    sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.DOWNLOAD_PDF,
    onSelected: () => {
    if (isOffline) {
    showOfflineModal();
    return;
    }
    if (!moneyRequestReport?.reportID) {
    return;
    }
    onPDFModalOpen?.();
    exportReportToPDF({reportID: moneyRequestReport.reportID});
    },
    },

    1. The Search bulk-action option for DOWNLOAD_PDF also sets its text to translate('common.downloadAsPDF'). This code already branches on selectedReportIDs.length inside onSelected (one report vs. many), but the visible label is a constant regardless of how many reports are selected, so it can never say "Download reports".

    if (isExpenseReportSearch && selectedReportIDs.length > 0) {
    options.push({
    icon: expensifyIcons.Download,
    text: translate('common.downloadAsPDF'),
    value: CONST.SEARCH.BULK_ACTION_TYPES.DOWNLOAD_PDF,
    shouldCloseModalOnSelect: true,
    onSelected: async () => {
    if (isOffline) {
    setIsOfflineModalVisible(true);
    return;
    }
    if (selectedReportIDs.length === 1) {
    const reportIDForPDF = selectedReportIDs.at(0);
    if (!reportIDForPDF) {
    return;
    }
    await exportReportToPDF({reportID: reportIDForPDF});
    setPdfReportID(reportIDForPDF);
    setIsPdfModalVisible(true);
    return;
    }
    exportReportsToPDF(selectedReportIDs);
    // Clear the selection now that the export has started. The ExportDownloadStatusManager shows the modal.
    selectAllMatchingItems(false);
    clearSelectedTransactions(undefined, true);
    },
    });
    }

    1. The underlying reason no singular/plural variant is possible: the common namespace only defines the fixed downloadAsPDF string (no downloadReport/downloadReports keys exist), mirroring the sibling downloadReceipts key added in feat: add download receipts option to report #96947.

    App/src/languages/en.ts

    Lines 465 to 466 in 07dca73

    downloadAsPDF: 'Download as PDF',
    downloadReceipts: 'Download receipts',

    What changes do you think we should make in order to solve the problem?

    1. Add the two new translation keys to every language file, alongside the existing downloadReceipts key in the common namespace.

    en.ts (src/languages/en.ts), after downloadAsPDF:

    App/src/languages/en.ts

    Lines 465 to 466 in 07dca73

    downloadAsPDF: 'Download as PDF',
    downloadReceipts: 'Download receipts',

    downloadReport: 'Download report',
    downloadReports: 'Download reports',

    es.ts (src/languages/es.ts):

    App/src/languages/es.ts

    Lines 407 to 408 in 07dca73

    downloadAsPDF: 'Descargar como PDF',
    downloadReceipts: 'Descargar recibos',

    downloadReport: 'Descargar informe',
    downloadReports: 'Descargar informes',

    The same two keys must be added to the other language files that define common.downloadReceipts (de.ts, el.ts, fr.ts, it.ts, ja.ts, nl.ts, pl.ts, pt-BR.ts, zh-hans.ts) with the appropriate translation, so the TranslationPaths type derived from en.ts resolves for every locale. The downloadAsPDF key can be kept if referenced elsewhere, or removed if these two call sites are its only consumers.

    2. Individual report page — always singular. In useExportActions.ts, change the DOWNLOAD_PDF entry's text:

    text: translate('common.downloadAsPDF'),

    text: translate('common.downloadReport'),

    3. Search bulk actions — singular/plural based on the number of selected reports. In useSearchBulkActions.ts, change the bulk DOWNLOAD_PDF option's text to pick the key from selectedReportIDs.length (this block is only pushed when selectedReportIDs.length > 0, and onSelected already treats length === 1 as the single-report case, so the label stays in sync with the actual export):

    text: translate('common.downloadAsPDF'),

    text: translate(selectedReportIDs.length === 1 ? 'common.downloadReport' : 'common.downloadReports'),

    This reuses existing translation infrastructure and the already-computed selectedReportIDs, adds no new helpers, and keeps the change minimal and consistent with how the neighboring downloadReceipts label is wired.

  12. trasnake87 commented on Sep 14, 2026

    @trasnake87
    Contributor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    The PDF download action is labeled "Download as PDF" in two places: the report's More menu and the Spend > Reports "Selected" bulk menu. On the report page it should say "Download report". In the bulk menu it should say "Download report" when one report is selected and "Download reports" when more than one is selected.

    What is the root cause of that problem?

    In the bulk menu, the option's label and its onSelected handler use different logic. The handler already picks a flow based on how many reports are selected. With exactly one (selectedReportIDs.length === 1) it calls the single-report exportReportToPDF and opens the PDF modal. With more than one it calls the batch exportReportsToPDF. The text field ignores that choice and always uses common.downloadAsPDF, so the same label covers two different flows:

    if (isExpenseReportSearch && selectedReportIDs.length > 0) {
    options.push({
    icon: expensifyIcons.Download,
    text: translate('common.downloadAsPDF'),
    value: CONST.SEARCH.BULK_ACTION_TYPES.DOWNLOAD_PDF,
    shouldCloseModalOnSelect: true,
    onSelected: async () => {
    if (isOffline) {
    setIsOfflineModalVisible(true);
    return;
    }
    if (selectedReportIDs.length === 1) {
    const reportIDForPDF = selectedReportIDs.at(0);
    if (!reportIDForPDF) {
    return;
    }
    await exportReportToPDF({reportID: reportIDForPDF});
    setPdfReportID(reportIDForPDF);
    setIsPdfModalVisible(true);
    return;
    }
    exportReportsToPDF(selectedReportIDs);
    // Clear the selection now that the export has started. The ExportDownloadStatusManager shows the modal.
    selectAllMatchingItems(false);
    clearSelectedTransactions(undefined, true);
    },

    The count this needs is already in the hook. selectedReportIDs is rebuilt from selectedReports on every render, and the option is only added when it has at least one entry. So the label never has to handle zero, only 1 or more than 1:

    const selectedReportIDs = Object.values(selectedReports)
    .map((report) => report.reportID)
    .filter((reportID) => reportID !== undefined);

    On the report page, the DOWNLOAD_PDF secondary action always exports moneyRequestReport.reportID. It can never act on more than one report, so it only needs the singular label and no count check. It still uses the old key:

    [CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF]: {
    value: CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF,
    text: translate('common.downloadAsPDF'),
    icon: expensifyIcons.Download,
    sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.DOWNLOAD_PDF,
    onSelected: () => {
    if (isOffline) {
    showOfflineModal();
    return;
    }
    if (!moneyRequestReport?.reportID) {
    return;
    }
    onPDFModalOpen?.();
    exportReportToPDF({reportID: moneyRequestReport.reportID});
    },

    Only one string is defined, with the PDF wording. It sits next to downloadReceipts, which #96947 added without touching this key:

    App/src/languages/en.ts

    Lines 465 to 466 in f777fab

    downloadAsPDF: 'Download as PDF',
    downloadReceipts: 'Download receipts',

    There are two more things a label-only change would leave behind:

    • Unused key: common.downloadAsPDF has no other users. No .tsx file reads it and no test checks its text. After both sites switch keys, it would sit unused in all 11 locale files.
    • Help-site docs: the help articles tell users to click "Download as PDF" in exactly these two flows, so the steps would no longer match the UI.

    What changes do you think we should make in order to solve the problem?

    1. Bulk menu: base the label on the same check as the handler. In src/hooks/useSearchBulkActions.ts, change the text at L2502 to use the same === 1 test as L2510. If someone changes one branch later, the mismatch is easy to spot in review:

    text: translate(selectedReportIDs.length === 1 ? 'common.downloadReport' : 'common.downloadReports'),

    This two-key length === 1 ? singular : plural pattern is already used for fixed-text labels elsewhere:

    text: translate(selectedRules.length === 1 ? 'expenseRulesPage.deleteRule.deleteSingle' : 'expenseRulesPage.deleteRule.deleteMultiple'),

    text: translate(selectedKeys.length === 1 ? 'workspace.reportFields.deleteValue' : 'workspace.reportFields.deleteValues'),

    I'd skip the {one, other} plural form here. That form is meant for strings that show a count, and the spec asks for two fixed labels with no number.

    2. Report page. In src/hooks/useExportActions.ts L275:

    text: translate('common.downloadReport'),

    3. Translations: replace the key instead of adding next to it. In src/languages/en.ts, replace downloadAsPDF with:

    downloadReport: 'Download report',
    downloadReports: 'Download reports',

    In es.ts, add downloadReport: 'Descargar informe' and downloadReports: 'Descargar informes'. The other locales (de, fr, it, ja, nl, pl, pt-BR, zh-hans, el) start with the "automatically generated" header, so regenerate them with scripts/generateTranslations.ts rather than editing them by hand. Then remove downloadAsPDF from all 11 files so the old string can't be used again.

    4. Tests: check the label in each branch. tests/unit/hooks/useSearchBulkActionsDownloadPDFTest.ts already has one test for the single-report flow and one for the multi-report flow, and it mocks translate as (key) => key. Add expect(pdfOption.text).toBe('common.downloadReport') to the single-report case (L257) and 'common.downloadReports' to the multi-report case (L361), and rename both test titles to match. If the label and handler ever drift apart, these tests will fail.

    5. Help site: update the steps. In How-to-Export-Reports.md, step 4 of the single-report steps becomes Download report and step 4 of the multiple-reports steps becomes Download reports:

    ## How to download a single report as a PDF
    1. In the navigation tabs (on the left on web, on the bottom on mobile), go to **Spend > Reports**.
    2. Open the report you want to download.
    3. Select **More**.
    4. Select **Download as PDF**.
    The PDF includes all expenses, attached receipts, and report notes.
    For reports on a Submit workspace, the PDF also includes an **Approve or Pay** button and QR code that let your approver open and approve the report online.
    ## How to download multiple reports as PDFs
    1. In the navigation tabs (on the left on web, on the bottom on mobile), go to **Spend > Reports**.
    2. Select the checkbox next to each report you want to download, or use the top checkbox to select all.
    3. Select **Selected** at the top.
    4. Select **Download as PDF**.

    In the same article, the modal notes at lines 131 and 136 describe the batch and single-report flows. Change them to Download reports (more than one report selected) and Download report (single report):

    This applies to the downloads that show a preparing status window, whether you start them from the **Reports** page or from a report's **More** menu: **Export** with any template other than **Basic export**, **Download receipts**, and **Download as PDF** when you selected more than one report.
    It does not apply to:
    - **Basic export**, which downloads straight to your device without a status window.
    - **Download as PDF** for a single report, which uses its own window that does not reappear after a refresh. Start the download again to get a new PDF.

    Search-and-Download-Expenses.md line 88 also becomes Download report.

    Out of scope: the "Export as PDF" option for Expensify Card settlement groups is a separate action with its own key, and it stays as it is.

  13. 53 remaining items

  14. added
    Awaiting PaymentAuto-added when associated PR is deployed to production
    and removed on Sep 27, 2026
  15. melvin-bot commented on Sep 27, 2026

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  16. melvin-bot commented on Sep 27, 2026

    @melvin-bot

    Payment Summary

    Resolving PRs:

    BugZero Checklist (@mallenexpensify)

    • I have confirmed assignees, roles, and Upwork contracts look correct
    • I have paid out Upwork contracts / manual NewDot requests
    • [BugZero Assignee] I have created a GH issue for creating/updating the regression test once above steps have been agreed upon
  17. mallenexpensify commented on Sep 28, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor: @ahmdshrif paid $250 via Upwork
    Contributor+: @abdulrahuman5196 due $125 via NewDot

    @ahmdshrif can you please accept the job below? Please reply here and tag me once you have.
    https://www.upwork.com/jobs/~022104705448339657192

    @abdulrahuman5196 due $125 cuz PR review wasn't needed.

    @mollfpr in the future, please add Bug, that'll trigger triage from Melvin then a C+ will be assigned to review. Also.. if/when needed, you can set the price of a job by prefacing the title with [$100] and so on. The standard price is currently $175 as of Sept 19th

  18. ahmdshrif commented on Sep 28, 2026

    @ahmdshrif
    Contributor

    @mallenexpensify, I haven’t revived the payment yet.

  19. mallenexpensify commented on Sep 28, 2026

    @mallenexpensify
    Contributor

    @ahmdshrif paid, summary updated above. Thx. (sorry, mixed up my keyboard shortcuts)

  20. ahmdshrif commented on Sep 29, 2026

    @ahmdshrif
    Contributor

    @mallenexpensify Thanks! Unfortunately the payment still hasn't come through. The Upwork contract was ended without any payment being released. Could you please send a new offer or a bonus payment so I can receive it? 🙏

  21. mallenexpensify commented on Oct 1, 2026

    @mallenexpensify
    Contributor

    @ahmdshrif apologies, something fishy is happening, this happened twice the past few days where a contract was closed with $0 (and I don't know why). I'll be more vigilant. Paid now

    Image
  22. laurenreidexpensify commented on Oct 6, 2026

    @laurenreidexpensify
    Contributor

    Approved $125 @abdulrahuman5196 New Expensify

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionDailyKSv2EngineeringExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions