Commit 4488cffd authored by Benni Mack's avatar Benni Mack
Browse files

[BUGFIX] Rename recordlist export functionality to "download"

To prevent confusion between the EXT:impexp functionality
and the recordlists' own export functionality, latter is
renamed to "download".

Resolves: #94511
Releases: master
Change-Id: I5e101e04da00c52fe176beaf764de41c0fac9245
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/69794

Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Jochen's avatarJochen <rothjochen@gmail.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Jochen's avatarJochen <rothjochen@gmail.com>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent b4b01526
......@@ -19,20 +19,20 @@ import Modal = require('TYPO3/CMS/Backend/Modal');
import {lll} from 'TYPO3/CMS/Core/lit-helper';
enum Selectors {
formatSelector = '.t3js-record-export-format-selector',
formatOptions = '.t3js-record-export-format-option'
formatSelector = '.t3js-record-download-format-selector',
formatOptions = '.t3js-record-download-format-option'
}
/**
* Module: TYPO3/CMS/Recordlist/RecordExportButton
* Module: TYPO3/CMS/Recordlist/RecordDownloadButton
*
* @example
* <typo3-recordlist-record-export-button url="/url/to/configuration/form" title="Export records" ok="Export" close="Cancel">
* <button>Export records/button>
* </typo3-recordlist-record-export-button>
* <typo3-recordlist-record-download-button url="/url/to/configuration/form" title="Download records" ok="Download" close="Cancel">
* <button>Download records/button>
* </typo3-recordlist-record-download-button>
*/
@customElement('typo3-recordlist-record-export-button')
class RecordExportButton extends LitElement {
@customElement('typo3-recordlist-record-download-button')
class RecordDownloadButton extends LitElement {
@property({type: String}) url: string;
@property({type: String}) title: string;
@property({type: String}) ok: string;
......@@ -42,7 +42,7 @@ class RecordExportButton extends LitElement {
super();
this.addEventListener('click', (e: Event): void => {
e.preventDefault();
this.showExportConfigurationModal();
this.showDownloadConfigurationModal();
});
}
......@@ -50,7 +50,7 @@ class RecordExportButton extends LitElement {
return html`<slot></slot>`;
}
private showExportConfigurationModal(): void {
private showDownloadConfigurationModal(): void {
if (!this.url) {
// Don't render modal in case no url is given
return;
......@@ -58,7 +58,7 @@ class RecordExportButton extends LitElement {
Modal.advanced({
content: this.url,
title: this.title || 'Export record',
title: this.title || 'Download records',
severity: SeverityEnum.notice,
size: Modal.sizes.small,
type: Modal.types.ajax,
......@@ -71,9 +71,9 @@ class RecordExportButton extends LitElement {
trigger: (): void => Modal.dismiss(),
},
{
text: this.ok || lll('button.ok') || 'Export',
text: this.ok || lll('button.ok') || 'Download',
btnClass: 'btn-' + Severity.getCssClass(SeverityEnum.info),
name: 'export',
name: 'download',
trigger: (): void => {
const form: HTMLFormElement = Modal.currentModal[0].querySelector('form');
form && form.submit();
......
.. include:: ../../Includes.txt
============================================
Feature: #94411 - Recordlist export settings
============================================
==============================================
Feature: #94411 - Recordlist download settings
==============================================
See :issue:`94411`
Description
===========
In :issue:`94366`, the record export functionality in the recordlist
module was improved. Since then, the export could be triggered via a
In :issue:`94366`, the record download functionality in the recordlist
module was improved. Since then, the download could be triggered via a
button in each tables' header and no longer just in the single table view.
The export however did still not allow to adjust any settings, such as
The download however did still not allow to adjust any settings, such as
the definition of a custom filename. Furthermore, only `csv` was available
as possible export format.
as possible download format.
Therefore, and to further improve the already existing record export
functionality, the export button in the tables' header does not longer
trigger the export directly, but opens a modal with various adjustable
export settings such as:
Therefore, and to further improve the already existing record download
functionality, the download button in the tables' header does not longer
trigger the download directly, but opens a modal with various adjustable
download settings such as:
* Selection of columns to export: All columns or selected columns
* Selection of columns to download: All columns or selected columns
* Selection of the record values format: Either raw database values or processed (resolved) values
* Definition of a custom filename
* Selection of the export format (e.g. `csv`)
* Selection of the download format (e.g. `csv`)
Also export format specific options are available, e.g. selection of
the delimiter for `csv` exports.
Also download format specific options are available, e.g. selection of
the delimiter for `csv` downloads.
In case your installation already defines related TSconfig options
(e.g. :typoscript:`mod.web_list.csvDelimiter`), they will be added
as default value to the configuration modal.
Besides introducing those settings, also `json` is now available as
an alternative export format, including a format specific option,
an alternative download format, including a format specific option,
which allows to define additional meta information to be included in
the export.
the download.
Impact
======
It's now possible to configure the export of records in the
It's now possible to configure the download of records in the
recordlist. Furthermore, the new format option `json` is available.
.. index:: Backend, ext:recordlist
......@@ -22,9 +22,9 @@ use TYPO3\CMS\Core\Tests\Acceptance\Support\Helper\ModalDialog;
use TYPO3\CMS\Core\Tests\Acceptance\Support\Helper\PageTree;
/**
* Cases concerning the record export functionality
* Cases concerning the record download functionality
*/
class RecordExportCest
class RecordDownloadCest
{
/**
* @param BackendTester $I
......@@ -41,25 +41,25 @@ class RecordExportCest
*/
public function recordsCanBeExported(BackendTester $I, PageTree $pageTree, ModalDialog $modalDialog): void
{
$I->wantToTest('whether records can be exported in the recordlist');
$I->wantToTest('whether records can be downloaded in the recordlist');
$I->amGoingTo('export a record');
$I->amGoingTo('download a record');
$I->click('List');
$I->waitForElementNotVisible('#nprogress');
$pageTree->openPath(['styleguide TCA demo']);
$I->wait(0.2);
$I->switchToContentFrame();
$I->canSee('Export');
$I->click('typo3-recordlist-record-export-button button');
$I->canSee('Download');
$I->click('typo3-recordlist-record-download-button button');
$modalDialog->canSeeDialog();
$I->canSee('Export Page:', ModalDialog::$openedModalSelector . ' .modal-title');
$I->fillField(ModalDialog::$openedModalSelector . ' input[name="filename"]', 'test-export');
$I->canSee('Download Page:', ModalDialog::$openedModalSelector . ' .modal-title');
$I->fillField(ModalDialog::$openedModalSelector . ' input[name="filename"]', 'test-download');
$I->canSee('CSV options', ModalDialog::$openedModalSelector . ' .modal-body h5');
$I->selectOption(ModalDialog::$openedModalSelector . ' select[name="format"]', 'json');
$I->dontSee('CSV options', ModalDialog::$openedModalSelector . ' .modal-body h5');
$I->see('JSON options', ModalDialog::$openedModalSelector . ' .modal-body h5');
$I->selectOption(ModalDialog::$openedModalSelector . ' select[name="json[meta]"]', 'full');
$I->click('button[name="export"]', ModalDialog::$openedModalButtonContainerSelector);
$I->click('button[name="download"]', ModalDialog::$openedModalButtonContainerSelector);
$I->waitForElementNotVisible(ModalDialog::$openedModalSelector, 30);
}
}
......@@ -126,7 +126,7 @@ class TableListViewHelper extends AbstractBackendViewHelper
$enableControlPanels = $this->arguments['enableControlPanels'];
$this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Recordlist/Recordlist');
$this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Recordlist/RecordExportButton');
$this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Recordlist/RecordDownloadButton');
$this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ActionDispatcher');
if ($enableControlPanels === true) {
$this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
......
......@@ -45,7 +45,7 @@ class DatabaseBrowser extends AbstractElementBrowser implements ElementBrowserIn
parent::initialize();
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/BrowseDatabase');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Tree/PageBrowser');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/RecordExportButton');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/RecordDownloadButton');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/ColumnSelectorButton');
}
......
......@@ -30,16 +30,16 @@ use TYPO3\CMS\Core\Utility\CsvUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
use TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList;
use TYPO3\CMS\Recordlist\RecordList\ExportRecordList;
use TYPO3\CMS\Recordlist\RecordList\DownloadRecordList;
/**
* Controller for handling exports of records, typically executed from the list module.
* Controller for handling download of records, typically executed from the list module.
*
* @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API.
*/
class RecordExportController
class RecordDownloadController
{
private const EXPORT_FORMATS = [
private const DOWNLOAD_FORMATS = [
'csv' => [
'options' => [
'delimiter' => [
......@@ -88,24 +88,24 @@ class RecordExportController
}
/**
* Handle record export request by evaluating the provided arguments,
* Handle record download request by evaluating the provided arguments,
* checking access, initializing the record list, fetching records and
* finally calling the requested export format action (e.g. csv).
* finally calling the requested download format action (e.g. csv).
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function handleExportRequest(ServerRequestInterface $request): ResponseInterface
public function handleDownloadRequest(ServerRequestInterface $request): ResponseInterface
{
$parsedBody = $request->getParsedBody();
$this->table = (string)($parsedBody['table'] ?? '');
if ($this->table === '') {
throw new \RuntimeException('No table was given for exporting records', 1623941276);
throw new \RuntimeException('No table was given for downloading records', 1623941276);
}
$this->format = (string)($parsedBody['format'] ?? '');
if ($this->format === '' || !isset(self::EXPORT_FORMATS[$this->format])) {
throw new \RuntimeException('No or an invalid export format given', 1624562166);
if ($this->format === '' || !isset(self::DOWNLOAD_FORMATS[$this->format])) {
throw new \RuntimeException('No or an invalid download format given', 1624562166);
}
$this->filename = $this->generateFilename((string)($parsedBody['filename'] ?? ''));
......@@ -121,7 +121,7 @@ class RecordExportController
$searchString = (string)($parsedBody['searchString'] ?? '');
$searchLevels = (int)($parsedBody['searchLevels'] ?? 0);
if (!is_array($pageinfo) && !($this->id === 0 && $searchString !== '' && $searchLevels !== 0)) {
throw new AccessDeniedException('Insufficient permissions for accessing this export', 1623941361);
throw new AccessDeniedException('Insufficient permissions for accessing this download', 1623941361);
}
// Initialize database record list
......@@ -137,16 +137,16 @@ class RecordExportController
$hideTranslations = ($this->modTSconfig['hideTranslations'] ?? '') === '*'
|| GeneralUtility::inList($this->modTSconfig['hideTranslations'] ?? '', $this->table);
// Initialize the exporter
$exporter = GeneralUtility::makeInstance(
ExportRecordList::class,
// Initialize the downloader
$downloader = GeneralUtility::makeInstance(
DownloadRecordList::class,
$recordList,
GeneralUtility::makeInstance(TranslationConfigurationProvider::class)
);
// Fetch and process the header row and the records
$headerRow = $exporter->getHeaderRow($columnsToRender);
$records = $exporter->getRecords(
$headerRow = $downloader->getHeaderRow($columnsToRender);
$records = $downloader->getRecords(
$this->table,
$this->id,
$columnsToRender,
......@@ -155,38 +155,38 @@ class RecordExportController
(bool)($parsedBody['rawValues'] ?? false)
);
$exportAction = $this->format . 'ExportAction';
return $this->{$exportAction}($request, $headerRow, $records);
$downloadAction = $this->format . 'DownloadAction';
return $this->{$downloadAction}($request, $headerRow, $records);
}
/**
* Generate settings form for the export request
* Generate settings form for the download request
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function exportSettingsAction(ServerRequestInterface $request): ResponseInterface
public function downloadSettingsAction(ServerRequestInterface $request): ResponseInterface
{
$exportArguments = $request->getQueryParams();
$downloadArguments = $request->getQueryParams();
$this->table = (string)($exportArguments['table'] ?? '');
$this->table = (string)($downloadArguments['table'] ?? '');
if ($this->table === '') {
throw new \RuntimeException('No table was given for exporting records', 1624551586);
throw new \RuntimeException('No table was given for downloading records', 1624551586);
}
$this->id = (int)($exportArguments['id'] ?? 0);
$this->id = (int)($downloadArguments['id'] ?? 0);
$this->modTSconfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['web_list.'] ?? [];
$view = GeneralUtility::makeInstance(StandaloneView::class);
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName(
'EXT:recordlist/Resources/Private/Templates/RecordExportSettings.html'
'EXT:recordlist/Resources/Private/Templates/RecordDownloadSettings.html'
));
$view->assignMultiple([
'formUrl' => $this->uriBuilder->buildUriFromRoute('record_export'),
'formUrl' => $this->uriBuilder->buildUriFromRoute('record_download'),
'table' => $this->table,
'exportArguments' => $exportArguments,
'formats' => array_keys(self::EXPORT_FORMATS),
'downloadArguments' => $downloadArguments,
'formats' => array_keys(self::DOWNLOAD_FORMATS),
'formatOptions' => $this->getFormatOptionsWithResolvedDefaults(),
]);
......@@ -198,14 +198,14 @@ class RecordExportController
}
/**
* Generating an export in CSV format
* Generating an download in CSV format
*
* @param ServerRequestInterface $request
* @param array $headerRow
* @param array $records
* @return ResponseInterface
*/
protected function csvExportAction(
protected function csvDownloadAction(
ServerRequestInterface $request,
array $headerRow,
array $records
......@@ -220,19 +220,18 @@ class RecordExportController
$result[] = CsvUtility::csvValues($record, $csvDelimiter, $csvQuote);
}
return $this->generateExportResponse(implode(CRLF, $result));
return $this->generateDownloadResponse(implode(CRLF, $result));
}
/**
* Generating an export in JSON format
* Generating an download in JSON format
*
* @param ServerRequestInterface $request
* @param array $headerRow
* @param array $records
*
* @return ResponseInterface
*/
protected function jsonExportAction(
protected function jsonDownloadAction(
ServerRequestInterface $request,
array $headerRow,
array $records
......@@ -274,7 +273,7 @@ class RecordExportController
break;
}
return $this->generateExportResponse(json_encode($result) ?: '');
return $this->generateDownloadResponse(json_encode($result) ?: '');
}
/**
......@@ -322,7 +321,7 @@ class RecordExportController
*/
protected function getFormatOptionsWithResolvedDefaults(): array
{
$formatOptions = self::EXPORT_FORMATS;
$formatOptions = self::DOWNLOAD_FORMATS;
if ($this->modTSconfig === []) {
return $formatOptions;
......@@ -361,7 +360,7 @@ class RecordExportController
?? $default;
}
protected function generateExportResponse(string $result): ResponseInterface
protected function generateDownloadResponse(string $result): ResponseInterface
{
$response = $this->responseFactory->createResponse()
->withHeader('Content-Type', 'application/octet-stream')
......
......@@ -109,7 +109,7 @@ class RecordListController
$this->moduleTemplate = $this->moduleTemplateFactory->create($request);
$this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/Recordlist');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/RecordExportButton');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/RecordDownloadButton');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/ColumnSelectorButton');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/ClearCache');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/AjaxDataHandler');
......
......@@ -669,8 +669,8 @@ class DatabaseRecordList
}
// Show the select box
$tableActions .= $this->columnSelector($table);
// Create the CSV Export button
$tableActions .= $this->createExportButtonForTable($table, $totalItems);
// Create the Download button
$tableActions .= $this->createDownloadButtonForTable($table, $totalItems);
}
// Render table rows only if in multi table view or if in single table view
$rowOutput = '';
......@@ -870,39 +870,39 @@ class DatabaseRecordList
. '</a>';
}
protected function createExportButtonForTable(string $table, int $totalItems): string
protected function createDownloadButtonForTable(string $table, int $totalItems): string
{
// Do not render the export button for page translations or in case export is disabled
// Do not render the download button for page translations or in case it is disabled
if (($this->modTSconfig['noExportRecordsLinks'] ?? false) || $this->showOnlyTranslatedRecords) {
return '';
}
$exportButtonLabel = $this->getLanguageService()->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_export.xlf:export');
$exportButtonTitle = sprintf($this->getLanguageService()->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_export.xlf:' . ($totalItems === 1 ? 'exportRecord' : 'exportRecords')), $totalItems);
$exportCancelTitle = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cancel');
$exportSettingsUrl = $this->uriBuilder->buildUriFromRoute(
'ajax_record_export_settings',
$downloadButtonLabel = $this->getLanguageService()->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_download.xlf:download');
$downloadButtonTitle = sprintf($this->getLanguageService()->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_download.xlf:' . ($totalItems === 1 ? 'downloadRecord' : 'downloadRecords')), $totalItems);
$downloadCancelTitle = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cancel');
$downloadSettingsUrl = $this->uriBuilder->buildUriFromRoute(
'ajax_record_download_settings',
['id' => $this->id, 'table' => $table, 'searchString' => $this->searchString, 'searchLevels' => $this->searchLevels]
);
$exportSettingsTitle = sprintf(
$this->getLanguageService()->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_export.xlf:' . ($totalItems === 1 ? 'exportRecordSettings' : 'exportRecordsSettings')),
$downloadSettingsTitle = sprintf(
$this->getLanguageService()->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_download.xlf:' . ($totalItems === 1 ? 'downloadRecordSettings' : 'downloadRecordsSettings')),
$this->getLanguageService()->sL($GLOBALS['TCA'][$table]['ctrl']['title'] ?? '') ?: $table,
$totalItems
);
return '
<div class="pull-right">
<typo3-recordlist-record-export-button
url="' . htmlspecialchars($exportSettingsUrl) . '"
title="' . htmlspecialchars($exportSettingsTitle) . '"
ok="' . htmlspecialchars($exportButtonTitle) . '"
close="' . htmlspecialchars($exportCancelTitle) . '"
<typo3-recordlist-record-download-button
url="' . htmlspecialchars($downloadSettingsUrl) . '"
title="' . htmlspecialchars($downloadSettingsTitle) . '"
ok="' . htmlspecialchars($downloadButtonTitle) . '"
close="' . htmlspecialchars($downloadCancelTitle) . '"
>
<button type="button" class="btn btn-default btn-sm me-2" title="' . htmlspecialchars($exportButtonTitle) . '">' .
$this->iconFactory->getIcon('actions-document-export-csv', Icon::SIZE_SMALL) . ' ' .
htmlspecialchars($exportButtonLabel) .
<button type="button" class="btn btn-default btn-sm me-2" title="' . htmlspecialchars($downloadButtonTitle) . '">' .
$this->iconFactory->getIcon('actions-database-export', Icon::SIZE_SMALL) . ' ' .
htmlspecialchars($downloadButtonLabel) .
'</button>
</typo3-recordlist-record-export-button>
</typo3-recordlist-record-download-button>
</div>';
}
......
......@@ -24,14 +24,14 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Fetches all records like in the list module but returns them as array in order to allow
* exports (e.g. CSV) in the Controller with prepared data.
* downloads (e.g. CSV) in the Controller with prepared data.
*
* This class acts as a composition-based wrapper for DatabaseRecordList for creating records
* ready to be exported.
* ready to be downloaded.
*
* @internal this class is not part of the TYPO3 Core API due to its nature as being a wrapper for DatabaseRecordList and a very specific implementation.
*/
class ExportRecordList
class DownloadRecordList
{
protected DatabaseRecordList $recordList;
protected TranslationConfigurationProvider $translationConfigurationProvider;
......@@ -125,7 +125,7 @@ class ExportRecordList
*
* @param string $table Table name
* @param mixed[] $row Current record
* @param string[] $columnsToRender the columns to be displayed / exported
* @param string[] $columnsToRender the columns to be displayed / downloaded
* @param int $pageId used for the legacy hook
* @param bool $rawValues Whether the field values should not be processed
* @return array the prepared row
......
......@@ -7,9 +7,9 @@ return [
'path' => '/web/list/clearpagecache',
'target' => \TYPO3\CMS\Recordlist\Controller\ClearPageCacheController::class . '::mainAction'
],
'record_export_settings' => [
'path' => '/record/export/settings',
'target' => \TYPO3\CMS\Recordlist\Controller\RecordExportController::class . '::exportSettingsAction'
'record_download_settings' => [
'path' => '/record/download/settings',
'target' => \TYPO3\CMS\Recordlist\Controller\RecordDownloadController::class . '::downloadSettingsAction'
],
'record_show_columns' => [
'path' => '/record/show/columns',
......
......@@ -8,9 +8,9 @@ return [
'path' => '/wizard/record/browse',
'target' => \TYPO3\CMS\Recordlist\Controller\ElementBrowserController::class . '::mainAction'
],
'record_export' => [
'path' => '/record/export',
'record_download' => [
'path' => '/record/download',
'methods' => ['POST'],
'target' => \TYPO3\CMS\Recordlist\Controller\RecordExportController::class . '::handleExportRequest'
'target' => \TYPO3\CMS\Recordlist\Controller\RecordDownloadController::class . '::handleDownloadRequest'
],
];
......@@ -10,7 +10,7 @@ services:
TYPO3\CMS\Recordlist\Controller\RecordListController:
tags: ['backend.controller']
TYPO3\CMS\Recordlist\Controller\RecordExportController:
TYPO3\CMS\Recordlist\Controller\RecordDownloadController:
tags: ['backend.controller']
TYPO3\CMS\Recordlist\Controller\ColumnSelectorController:
......
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="EXT:recordlist/Resources/Private/Language/locallang_download.xlf" date="2012-06-17T10:22:33Z" product-name="recordlist">
<header/>
<body>
<trans-unit id="downloadRecord" resname="downloadRecord">
<source>Download %d record</source>
</trans-unit>
<trans-unit id="downloadRecords" resname="downloadRecords">
<source>Download %d records</source>
</trans-unit>
<trans-unit id="download" resname="download">
<source>Download</source>
</trans-unit>
<trans-unit id="downloadRecordSettings" resname="downloadRecordSettings">
<source>Download %s: %d Record</source>
</trans-unit>
<trans-unit id="downloadRecordsSettings" resname="downloadRecordsSettings">
<source>Download %s: %d Records</source>
</trans-unit>
<trans-unit id="downloadSettings.generalSettings" resname="downloadSettings.generalSettings">
<source>General settings</source>
</trans-unit>
<trans-unit id="downloadSettings.columnsToDownload" resname="downloadSettings.columnsToDownload">
<source>Columns to download</source>
</trans-unit>