Commit 630e26c7 authored by Andreas Fernandez's avatar Andreas Fernandez Committed by Mathias Brodala
Browse files

[!!!][TASK] Make localization wizard independent of colPos

The localization wizard in the page module offers now a global translate
action per page only. With this change, a user isn't anymore able to
translate content on column basis.

This streamlines the localization process as it lowers the risk of
creating broken localization setups (a.k.a "mixed mode") and it will
simplify the upcoming "Change mode" wizard that enables to switch
between "Free" and "Connected" modes.

Resolves: #84877
Releases: master
Change-Id: Ibfd4641c5a8d3622c86b5a8657af00b2b3122503
Reviewed-on: https://review.typo3.org/56813

Tested-by: default avatarTYPO3com <no-reply@typo3.com>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Mathias Brodala's avatarMathias Brodala <mbrodala@pagemachine.de>
Tested-by: Mathias Brodala's avatarMathias Brodala <mbrodala@pagemachine.de>
parent b2ebd754
$fieldset-padding: $padding-small-vertical;
$option-margin-bottom: $padding-small-horizontal;
.localization-wizard {
......@@ -15,4 +16,16 @@ $option-margin-bottom: $padding-small-horizontal;
font-weight: normal;
}
}
.localization-fieldset {
margin-bottom: $padding-large-vertical;
> label {
padding: 0 $fieldset-padding;
input[type="checkbox"] {
margin: 0 $padding-base-horizontal;
}
}
}
}
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Controller\Page;
/*
......@@ -19,6 +21,7 @@ use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
use TYPO3\CMS\Backend\Domain\Repository\Localization\LocalizationRepository;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendLayoutView;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Http\Response;
......@@ -29,6 +32,8 @@ use TYPO3\CMS\Core\Versioning\VersionState;
/**
* LocalizationController handles the AJAX requests for record localization
*
* @internal
*/
class LocalizationController
{
......@@ -62,20 +67,19 @@ class LocalizationController
}
/**
* Get used languages in a colPos of a page
* Get used languages in a page
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function getUsedLanguagesInPageAndColumn(ServerRequestInterface $request): ResponseInterface
public function getUsedLanguagesInPage(ServerRequestInterface $request): ResponseInterface
{
$params = $request->getQueryParams();
if (!isset($params['pageId'], $params['colPos'], $params['languageId'])) {
if (!isset($params['pageId'], $params['languageId'])) {
return new JsonResponse(null, 400);
}
$pageId = (int)$params['pageId'];
$colPos = (int)$params['colPos'];
$languageId = (int)$params['languageId'];
/** @var TranslationConfigurationProvider $translationProvider */
......@@ -85,10 +89,10 @@ class LocalizationController
$availableLanguages = [];
// First check whether column has localized records
$elementsInColumnCount = $this->localizationRepository->getLocalizedRecordCount($pageId, $colPos, $languageId);
$elementsInColumnCount = $this->localizationRepository->getLocalizedRecordCount($pageId, $languageId);
if ($elementsInColumnCount === 0) {
$fetchedAvailableLanguages = $this->localizationRepository->fetchAvailableLanguages($pageId, $colPos, $languageId);
$fetchedAvailableLanguages = $this->localizationRepository->fetchAvailableLanguages($pageId, $languageId);
$availableLanguages[] = $systemLanguages[0];
foreach ($fetchedAvailableLanguages as $language) {
......@@ -97,7 +101,7 @@ class LocalizationController
}
}
} else {
$result = $this->localizationRepository->fetchOriginLanguage($pageId, $colPos, $languageId);
$result = $this->localizationRepository->fetchOriginLanguage($pageId, $languageId);
$availableLanguages[] = $systemLanguages[$result['sys_language_uid']];
}
......@@ -122,16 +126,19 @@ class LocalizationController
public function getRecordLocalizeSummary(ServerRequestInterface $request): ResponseInterface
{
$params = $request->getQueryParams();
if (!isset($params['pageId'], $params['colPos'], $params['destLanguageId'], $params['languageId'])) {
if (!isset($params['pageId'], $params['destLanguageId'], $params['languageId'])) {
return new JsonResponse(null, 400);
}
$pageId = (int)$params['pageId'];
$destLanguageId = (int)$params['destLanguageId'];
$languageId = (int)$params['languageId'];
$records = [];
$result = $this->localizationRepository->getRecordsToCopyDatabaseResult(
$params['pageId'],
$params['colPos'],
$params['destLanguageId'],
$params['languageId'],
$pageId,
$destLanguageId,
$languageId,
'*'
);
......@@ -140,14 +147,21 @@ class LocalizationController
if (!$row || VersionState::cast($row['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
continue;
}
$records[] = [
$colPos = $row['colPos'];
if (!isset($records[$colPos])) {
$records[$colPos] = [];
}
$records[$colPos][] = [
'icon' => $this->iconFactory->getIconForRecord('tt_content', $row, Icon::SIZE_SMALL)->render(),
'title' => $row[$GLOBALS['TCA']['tt_content']['ctrl']['label']],
'uid' => $row['uid']
];
}
return (new JsonResponse())->setPayload($records);
return (new JsonResponse())->setPayload([
'records' => $records,
'columns' => $this->getPageColumns($pageId),
]);
}
/**
......@@ -170,7 +184,6 @@ class LocalizationController
// Filter transmitted but invalid uids
$params['uidList'] = $this->filterInvalidUids(
(int)$params['pageId'],
(int)$params['colPos'],
(int)$params['destLanguageId'],
(int)$params['srcLanguageId'],
$params['uidList']
......@@ -186,7 +199,6 @@ class LocalizationController
* be smuggled in.
*
* @param int $pageId
* @param int $colPos
* @param int $destLanguageId
* @param int $srcLanguageId
* @param array $transmittedUidList
......@@ -194,7 +206,6 @@ class LocalizationController
*/
protected function filterInvalidUids(
int $pageId,
int $colPos,
int $destLanguageId,
int $srcLanguageId,
array $transmittedUidList
......@@ -202,7 +213,6 @@ class LocalizationController
// Get all valid uids that can be processed
$validUidList = $result = $this->localizationRepository->getRecordsToCopyDatabaseResult(
$pageId,
$colPos,
$destLanguageId,
$srcLanguageId,
'uid'
......@@ -216,7 +226,7 @@ class LocalizationController
*
* @param array $params
*/
protected function process($params)
protected function process($params): void
{
$destLanguageId = (int)$params['destLanguageId'];
......@@ -243,4 +253,24 @@ class LocalizationController
$dataHandler->start([], $cmd);
$dataHandler->process_cmdmap();
}
/**
* @param int $pageId
* @return array
*/
protected function getPageColumns(int $pageId): array
{
$columns = [];
$backendLayoutView = GeneralUtility::makeInstance(BackendLayoutView::class);
$backendLayouts = $backendLayoutView->getSelectedBackendLayout($pageId);
foreach ($backendLayouts['__items'] as $backendLayout) {
$columns[(int)$backendLayout[1]] = $backendLayout[0];
}
return [
'columns' => $columns,
'columnList' => $backendLayouts['__colPosList'],
];
}
}
......@@ -640,8 +640,9 @@ class PageLayoutController
/**
* @return string $title
* @internal
*/
protected function getLocalizedPageTitle()
public function getLocalizedPageTitle()
{
if ($this->current_sys_language > 0) {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
......
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\Domain\Repository\Localization;
/*
......@@ -14,6 +16,8 @@ namespace TYPO3\CMS\Backend\Domain\Repository\Localization;
* The TYPO3 project - inspiring people to share!
*/
use Doctrine\DBAL\Driver\Statement;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
......@@ -23,26 +27,23 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Repository for record localizations
*
* @internal
*/
class LocalizationRepository
{
/**
* Fetch the language from which the records of a colPos in a certain language were initially localized
* Fetch the language from which the records in a certain language were initially localized
*
* @param int $pageId
* @param int $colPos
* @param int $localizedLanguage
* @return array|false
* @return array
*/
public function fetchOriginLanguage($pageId, $colPos, $localizedLanguage)
public function fetchOriginLanguage(int $pageId, int $localizedLanguage): array
{
$queryBuilder = $this->getQueryBuilderWithWorkspaceRestriction('tt_content');
$constraints = [
$queryBuilder->expr()->eq(
'tt_content.colPos',
$queryBuilder->createNamedParameter($colPos, \PDO::PARAM_INT)
),
$queryBuilder->expr()->eq(
'tt_content.pid',
$queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
......@@ -77,19 +78,18 @@ class LocalizationRepository
->where(...$constraints)
->groupBy('tt_content_orig.sys_language_uid');
return $queryBuilder->execute()->fetch();
return $queryBuilder->execute()->fetch() ?: [];
}
/**
* Returns number of localized records in given page, colPos and language
* Returns number of localized records in given page and language
* Records which were added to the language directly (not through translation) are not counted.
*
* @param int $pageId
* @param int $colPos
* @param int $languageId
* @return int
*/
public function getLocalizedRecordCount($pageId, $colPos, $languageId)
public function getLocalizedRecordCount(int $pageId, int $languageId): int
{
$queryBuilder = $this->getQueryBuilderWithWorkspaceRestriction('tt_content');
......@@ -100,10 +100,6 @@ class LocalizationRepository
'tt_content.sys_language_uid',
$queryBuilder->createNamedParameter($languageId, \PDO::PARAM_INT)
),
$queryBuilder->expr()->eq(
'tt_content.colPos',
$queryBuilder->createNamedParameter($colPos, \PDO::PARAM_INT)
),
$queryBuilder->expr()->eq(
'tt_content.pid',
$queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
......@@ -123,11 +119,10 @@ class LocalizationRepository
* Fetch all available languages
*
* @param int $pageId
* @param int $colPos
* @param int $languageId
* @return array
*/
public function fetchAvailableLanguages($pageId, $colPos, $languageId)
public function fetchAvailableLanguages(int $pageId, int $languageId): array
{
$queryBuilder = $this->getQueryBuilderWithWorkspaceRestriction('tt_content');
......@@ -136,10 +131,6 @@ class LocalizationRepository
'tt_content.sys_language_uid',
$queryBuilder->quoteIdentifier('sys_language.uid')
),
$queryBuilder->expr()->eq(
'tt_content.colPos',
$queryBuilder->createNamedParameter($colPos, \PDO::PARAM_INT)
),
$queryBuilder->expr()->eq(
'tt_content.pid',
$queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
......@@ -201,17 +192,16 @@ class LocalizationRepository
* Get records for copy process
*
* @param int $pageId
* @param int $colPos
* @param int $destLanguageId
* @param int $languageId
* @param string $fields
* @return \Doctrine\DBAL\Driver\Statement
* @return Statement
*/
public function getRecordsToCopyDatabaseResult($pageId, $colPos, $destLanguageId, $languageId, $fields = '*')
public function getRecordsToCopyDatabaseResult(int $pageId, int $destLanguageId, int $languageId, string $fields = '*'): Statement
{
$originalUids = [];
// Get original uid of existing elements triggered language / colpos
// Get original uid of existing elements triggered language
$queryBuilder = $this->getQueryBuilderWithWorkspaceRestriction('tt_content');
$originalUidsStatement = $queryBuilder
......@@ -222,10 +212,6 @@ class LocalizationRepository
'sys_language_uid',
$queryBuilder->createNamedParameter($destLanguageId, \PDO::PARAM_INT)
),
$queryBuilder->expr()->eq(
'tt_content.colPos',
$queryBuilder->createNamedParameter($colPos, \PDO::PARAM_INT)
),
$queryBuilder->expr()->eq(
'tt_content.pid',
$queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
......@@ -244,10 +230,6 @@ class LocalizationRepository
'tt_content.sys_language_uid',
$queryBuilder->createNamedParameter($languageId, \PDO::PARAM_INT)
),
$queryBuilder->expr()->eq(
'tt_content.colPos',
$queryBuilder->createNamedParameter($colPos, \PDO::PARAM_INT)
),
$queryBuilder->expr()->eq(
'tt_content.pid',
$queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
......@@ -271,9 +253,9 @@ class LocalizationRepository
/**
* Returns the current BE user.
*
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
* @return BackendUserAuthentication
*/
protected function getBackendUser()
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
......
......@@ -913,13 +913,14 @@ class PageLayoutView implements LoggerAwareInterface
$cList = array_keys($contentRecordsPerColumn);
// For each column, render the content into a variable:
foreach ($cList as $columnId) {
if (!isset($this->contentElementCache[$lP][$columnId])) {
$this->contentElementCache[$lP][$columnId] = [];
if (!isset($this->contentElementCache[$lP])) {
$this->contentElementCache[$lP] = [];
}
if (!$lP) {
$defaultLanguageElementsByColumn[$columnId] = [];
}
// Start wrapping div
$content[$columnId] .= '<div data-colpos="' . $columnId . '" data-language-uid="' . $lP . '" class="t3js-sortable t3js-sortable-lang t3js-sortable-lang-' . $lP . ' t3-page-ce-wrapper';
if (empty($contentRecordsPerColumn[$columnId])) {
......@@ -999,13 +1000,6 @@ class PageLayoutView implements LoggerAwareInterface
$this->contentElementCache[$lP][$columnId][$row['uid']] = $row;
if ($this->tt_contentConfig['languageMode']) {
$languageColumn[$columnId][$lP] = $head[$columnId] . $content[$columnId];
if (!$this->defLangBinding && $columnId !== 'unused') {
$languageColumn[$columnId][$lP] .= $this->newLanguageButton(
$this->getNonTranslatedTTcontentUids($defaultLanguageElementsByColumn[$columnId], $id, $lP),
$lP,
$columnId
);
}
}
if (is_array($row) && !VersionState::cast($row['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
$singleElementHTML = '';
......@@ -1134,13 +1128,7 @@ class PageLayoutView implements LoggerAwareInterface
foreach ($cList as $columnId) {
if (GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnId) || $columnId === 'unused') {
$languageColumn[$columnId][$lP] = $head[$columnId] . $content[$columnId];
if (!$this->defLangBinding && $columnId !== 'unused') {
$languageColumn[$columnId][$lP] .= $this->newLanguageButton(
$this->getNonTranslatedTTcontentUids($defaultLanguageElementsByColumn[$columnId], $id, $lP),
$lP,
$columnId
);
}
// We sort $languageColumn again according to $cList as it may contain data already from above.
$sortedLanguageColumn[$columnId] = $languageColumn[$columnId];
}
......@@ -1348,11 +1336,27 @@ class PageLayoutView implements LoggerAwareInterface
);
$lPLabel =
'<div class="btn-group">'
'<p>' . $recordIcon . ' ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($pageLocalizationRecord['title'], 20)) . '</p>'
. '<div class="btn-group">'
. $viewLink
. $editLink
. '</div>'
. ' ' . $recordIcon . ' ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($pageLocalizationRecord['title'], 20));
;
$defaultLanguageElements = [];
array_walk($defaultLanguageElementsByColumn, function (array $columnContent) use (&$defaultLanguageElements) {
$defaultLanguageElements = array_merge($defaultLanguageElements, $columnContent);
});
$localizationButtons = [];
$localizationButtons[] = $this->newLanguageButton(
$this->getNonTranslatedTTcontentUids($defaultLanguageElements, $id, $lP),
$lP
);
if (!empty($localizationButtons)) {
$lPLabel .= LF . '<div class="btn-group">' . implode(LF, $localizationButtons) . '</div>';
}
} else {
$editLink = '';
$recordIcon = '';
......@@ -1419,11 +1423,7 @@ class PageLayoutView implements LoggerAwareInterface
} else {
$element = $defLangBinding[$cKey][$lP][$defUid] ?? '';
}
$cCont[] = $element . $this->newLanguageButton(
$this->getNonTranslatedTTcontentUids([$defUid], $id, $lP),
$lP,
$cKey
);
$cCont[] = $element;
}
$out .= '
<tr>
......@@ -2397,10 +2397,9 @@ class PageLayoutView implements LoggerAwareInterface
*
* @param array $defaultLanguageUids Numeric array with uids of tt_content elements in the default language
* @param int $lP Sys language UID
* @param int $colPos Column position
* @return string "Copy languages" button, if available.
*/
public function newLanguageButton($defaultLanguageUids, $lP, $colPos = 0)
public function newLanguageButton($defaultLanguageUids, $lP)
{
$lP = (int)$lP;
if (!$this->doEdit || !$lP) {
......@@ -2420,35 +2419,38 @@ class PageLayoutView implements LoggerAwareInterface
}
}
if (isset($this->contentElementCache[$lP][$colPos]) && is_array($this->contentElementCache[$lP][$colPos])) {
foreach ($this->contentElementCache[$lP][$colPos] as $record) {
$key = array_search($record['l10n_source'], $defaultLanguageUids);
if ($key !== false) {
unset($defaultLanguageUids[$key]);
if (isset($this->contentElementCache[$lP]) && is_array($this->contentElementCache[$lP])) {
foreach ($this->contentElementCache[$lP] as $column => $records) {
foreach ($records as $record) {
$key = array_search($record['l10n_source'], $defaultLanguageUids);
if ($key !== false) {
unset($defaultLanguageUids[$key]);
}
}
}
}
if (!empty($defaultLanguageUids)) {
$theNewButton =
'<input'
. ' class="btn btn-default t3js-localize"'
. ' type="button"'
. ' disabled'
. ' value="' . htmlspecialchars($this->getLanguageService()->getLL('newPageContent_translate')) . '"'
. ' data-has-elements="' . (int)!empty($this->contentElementCache[$lP][$colPos]) . '"'
'<a'
. ' href="#"'
. ' class="btn btn-default btn-sm t3js-localize disabled"'
. ' title="' . htmlspecialchars($this->getLanguageService()->getLL('newPageContent_translate')) . '"'
. ' data-page="' . htmlspecialchars($this->getPageLayoutController()->getLocalizedPageTitle()) . '"'
. ' data-has-elements="' . (int)!empty($this->contentElementCache[$lP]) . '"'
. ' data-allow-copy="' . (int)$allowCopy . '"'
. ' data-allow-translate="' . (int)$allowTranslate . '"'
. ' data-table="tt_content"'
. ' data-page-id="' . (int)GeneralUtility::_GP('id') . '"'
. ' data-language-id="' . $lP . '"'
. ' data-language-name="' . htmlspecialchars($this->tt_contentConfig['languageCols'][$lP]) . '"'
. ' data-colpos-id="' . $colPos . '"'
. ' data-colpos-name="' . BackendUtility::getProcessedValue('tt_content', 'colPos', $colPos) . '"'
. '/>';
. '>'
. $this->iconFactory->getIcon('actions-localize', Icon::SIZE_SMALL)->render()
. ' ' . htmlspecialchars($this->getLanguageService()->getLL('newPageContent_translate'))
. '</a>';
}
return '<div class="t3-page-lang-copyce">' . $theNewButton . '</div>';
return $theNewButton;
}
/**
......
......@@ -246,10 +246,10 @@ return [
'target' => \TYPO3\CMS\Backend\Controller\LinkBrowserController::class . '::encodeTypoLink',
],
// Get languages in page and colPos
'languages_page_colpos' => [
// Get languages in page
'page_languages' => [
'path' => '/records/localize/get-languages',
'target' => Controller\Page\LocalizationController::class . '::getUsedLanguagesInPageAndColumn'
'target' => Controller\Page\LocalizationController::class . '::getUsedLanguagesInPage'
],
// Get summary of records to localize
......
......@@ -90,18 +90,6 @@
<trans-unit id="newPageContent_translate">
<source>Translate</source>
</trans-unit>
<trans-unit id="newPageContent_copyForLang">
<source>Translate default content elements</source>
</trans-unit>
<trans-unit id="newPageContent_translateFromDefault">
<source>Copy from default language</source>
</trans-unit>
<trans-unit id="newPageContent_copyFromAnotherLang_button">
<source>Copy from language...</source>
</trans-unit>
<trans-unit id="newPageContent_copyFromAnotherLang">
<source>Copy from language "%s"</source>
</trans-unit>
<trans-unit id="newPageContent2">
<source>New content</source>
</trans-unit>
......@@ -336,6 +324,9 @@
<trans-unit id="localize.wizard.header">
<source>Localize record &quot;{0}&quot; into {1}</source>
</trans-unit>
<trans-unit id="localize.wizard.header_page">
<source>Localize page &quot;{0}&quot; into {1}</source>
</trans-unit>
<trans-unit id="localize.wizard.button.cancel">
<source>Cancel</source>
</trans-unit>
......
......@@ -68,11 +68,13 @@ define([
Icons.getIcon('actions-edit-copy', Icons.sizes.large).done(function(copyIconMarkup) {
Localization.actions.translate.prepend(localizeIconMarkup);
Localization.actions.copy.prepend(copyIconMarkup);
$(Localization.identifier.triggerButton).prop('disabled', false);
$(Localization.identifier.triggerButton).removeClass('disabled');
});
});