[TASK] Overhaul record localization of Page module 60/44460/17
authorAndreas Fernandez <a.fernandez@scripting-base.de>
Sat, 31 Oct 2015 16:56:57 +0000 (17:56 +0100)
committerAlexander Opitz <opitz.alexander@googlemail.com>
Fri, 6 Nov 2015 08:22:11 +0000 (09:22 +0100)
This patch overhauls the record localization in the page module. The
"Translate" button opens now a modal window in which a user can choose
between translating and copying records into another language. This
replaces the split button introduced with 7.4.

Also, this patch removes the ``copyRecordFromLanguage`` method from
DataHandler again which was initially written for this purpose.

Resolves: #71224
Resolves: #70781
Releases: master
Change-Id: I3d61cbdfd6a1ccb7a5c93883634596ef62cc08f6
Reviewed-on: https://review.typo3.org/44460
Reviewed-by: Michael Oehlhof <typo3@oehlhof.de>
Tested-by: Michael Oehlhof <typo3@oehlhof.de>
Reviewed-by: Mathias Schreiber <mathias.schreiber@wmdb.de>
Tested-by: Mathias Schreiber <mathias.schreiber@wmdb.de>
Reviewed-by: Alexander Opitz <opitz.alexander@googlemail.com>
Tested-by: Alexander Opitz <opitz.alexander@googlemail.com>
Build/Resources/Public/Less/TYPO3/_wizard_localization.less [new file with mode: 0644]
Build/Resources/Public/Less/backend.less
typo3/sysext/backend/Classes/Controller/Page/LocalizationController.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Controller/PageLayoutController.php
typo3/sysext/backend/Classes/View/PageLayoutView.php
typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php
typo3/sysext/backend/Resources/Private/Language/locallang_layout.xlf
typo3/sysext/backend/Resources/Public/JavaScript/Localization.js [new file with mode: 0644]
typo3/sysext/core/Classes/DataHandling/DataHandler.php
typo3/sysext/t3skin/Resources/Public/Css/backend.css

diff --git a/Build/Resources/Public/Less/TYPO3/_wizard_localization.less b/Build/Resources/Public/Less/TYPO3/_wizard_localization.less
new file mode 100644 (file)
index 0000000..812dc99
--- /dev/null
@@ -0,0 +1,7 @@
+@option-margin-horizontal: @padding-small-horizontal;
+
+.localization-wizard {
+       .option {
+               margin-bottom: @option-margin-bottom;
+       }
+}
\ No newline at end of file
index 14a93e8..caa6c5f 100644 (file)
@@ -75,6 +75,8 @@
 @import "TYPO3/_module_lang.less";
 @import "TYPO3/_module_beuser.less";
 
+@import "TYPO3/_wizard_localization.less";
+
 @import "TYPO3/structure/_element_version.less";
 @import "TYPO3/structure/_element_wizard.less";
 @import "TYPO3/structure/_module_web_list.less";
diff --git a/typo3/sysext/backend/Classes/Controller/Page/LocalizationController.php b/typo3/sysext/backend/Classes/Controller/Page/LocalizationController.php
new file mode 100644 (file)
index 0000000..7a11155
--- /dev/null
@@ -0,0 +1,290 @@
+<?php
+namespace TYPO3\CMS\Backend\Controller\Page;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\DataHandling\DataHandler;
+use TYPO3\CMS\Core\Imaging\Icon;
+use TYPO3\CMS\Core\Imaging\IconFactory;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * LocalizationController handles the AJAX requests for record localization
+ */
+class LocalizationController
+{
+    /**
+     * @var IconFactory
+     */
+    protected $iconFactory;
+
+    /**
+     * Constructor
+     */
+    public function __construct()
+    {
+        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
+    }
+
+    /**
+     * Get used languages in a colPos of a page
+     *
+     * @param ServerRequestInterface $request
+     * @param ResponseInterface $response
+     * @return ResponseInterface
+     */
+    public function getUsedLanguagesInPageAndColumn(ServerRequestInterface $request, ResponseInterface $response)
+    {
+        $params = $request->getQueryParams();
+        if (!isset($params['pageId'], $params['colPos'], $params['languageId'])) {
+            $response = $response->withStatus(500);
+            return $response;
+        }
+
+        $pageId = (int)$params['pageId'];
+        $colPos = (int)$params['colPos'];
+        $languageId = (int)$params['languageId'];
+        $databaseConnection = $this->getDatabaseConnection();
+
+        /** @var TranslationConfigurationProvider $translationProvider */
+        $translationProvider = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
+        $systemLanguages = $translationProvider->getSystemLanguages($pageId);
+        $availableLanguages = [];
+        $availableLanguages[0] = $systemLanguages[0];
+
+        $excludeQueryPart = BackendUtility::deleteClause('tt_content')
+            . ($this->getBackendUser()->isAdmin() ? '' : ' AND sys_language.hidden=0')
+            . BackendUtility::versioningPlaceholderClause('tt_content');
+
+        // First check whether column is empty and then load additional languages
+        $elementsInColumnCount = $databaseConnection->exec_SELECTcountRows(
+            'uid',
+            'tt_content',
+            'tt_content.sys_language_uid=' . $languageId
+                . ' AND tt_content.colPos = ' . $colPos
+                . ' AND tt_content.pid=' . $pageId
+                . $excludeQueryPart
+        );
+        if ($elementsInColumnCount === 0) {
+            $res = $databaseConnection->exec_SELECTquery(
+                'sys_language.uid',
+                'tt_content,sys_language',
+                'tt_content.sys_language_uid=sys_language.uid'
+                    . ' AND tt_content.colPos = ' . $colPos
+                    . ' AND tt_content.pid=' . $pageId
+                    . ' AND sys_language.uid <> ' . $languageId
+                    . $excludeQueryPart,
+                'tt_content.sys_language_uid',
+                'sys_language.title'
+            );
+            while ($row = $databaseConnection->sql_fetch_assoc($res)) {
+                if (isset($systemLanguages[$row['uid']])) {
+                    $availableLanguages[] = $systemLanguages[$row['uid']];
+                }
+            }
+            $databaseConnection->sql_free_result($res);
+        }
+
+        // Pre-render all flag icons
+        foreach ($availableLanguages as &$language) {
+            if ($language['flagIcon'] === 'empty-empty') {
+                $language['flagIcon'] = '';
+            } else {
+                $language['flagIcon'] = $this->iconFactory->getIcon($language['flagIcon'], Icon::SIZE_SMALL)->render();
+            }
+        }
+
+        $response->getBody()->write(json_encode($availableLanguages));
+        return $response;
+    }
+
+    /**
+     * Get a prepared summary of records being translated
+     *
+     * @param ServerRequestInterface $request
+     * @param ResponseInterface $response
+     * @return ResponseInterface
+     */
+    public function getRecordLocalizeSummary(ServerRequestInterface $request, ResponseInterface $response)
+    {
+        $params = $request->getQueryParams();
+        if (!isset($params['pageId'], $params['colPos'], $params['languageId'])) {
+            $response = $response->withStatus(500);
+            return $response;
+        }
+
+        $records = [];
+        $databaseConnection = $this->getDatabaseConnection();
+        $res = $this->getRecordsToCopyDatabaseResult($params['pageId'], $params['colPos'], $params['languageId'], '*');
+        while ($row = $databaseConnection->sql_fetch_assoc($res)) {
+            $records[] = [
+                'icon' => $this->iconFactory->getIconForRecord('tt_content', $row, Icon::SIZE_SMALL)->render(),
+                'title' => $row[$GLOBALS['TCA']['tt_content']['ctrl']['label']],
+                'uid' => $row['uid']
+            ];
+        }
+        $databaseConnection->sql_free_result($res);
+
+        $response->getBody()->write(json_encode($records));
+        return $response;
+    }
+
+    /**
+     * @param ServerRequestInterface $request
+     * @param ResponseInterface $response
+     * @return ResponseInterface
+     */
+    public function getRecordUidsToCopy(ServerRequestInterface $request, ResponseInterface $response)
+    {
+        $params = $request->getQueryParams();
+        if (!isset($params['pageId'], $params['colPos'], $params['languageId'])) {
+            $response = $response->withStatus(500);
+            return $response;
+        }
+
+        $pageId = (int)$params['pageId'];
+        $colPos = (int)$params['colPos'];
+        $languageId = (int)$params['languageId'];
+        $databaseConnection = $this->getDatabaseConnection();
+
+        $res = $this->getRecordsToCopyDatabaseResult($pageId, $colPos, $languageId, 'uid');
+        $uids = [];
+        while ($row = $databaseConnection->sql_fetch_assoc($res)) {
+            $uids[] = (int)$row['uid'];
+        }
+        $databaseConnection->sql_free_result($res);
+
+        $response->getBody()->write(json_encode($uids));
+        return $response;
+    }
+
+    /**
+     * @param ServerRequestInterface $request
+     * @param ResponseInterface $response
+     * @return ResponseInterface
+     */
+    public function localizeRecords(ServerRequestInterface $request, ResponseInterface $response)
+    {
+        $params = $request->getQueryParams();
+        if (!isset($params['pageId'], $params['srcLanguageId'], $params['destLanguageId'], $params['action'], $params['uidList'])) {
+            $response = $response->withStatus(500);
+            return $response;
+        }
+
+        if ($params['action'] !== 'copyFromLanguage' && $params['action'] !== 'localize') {
+            $response->getBody()->write('Invalid action "' . $params['action'] . '" called.');
+            $response = $response->withStatus(500);
+            return $response;
+        }
+
+        $pageId = (int)$params['pageId'];
+        $srcLanguageId = (int)$params['srcLanguageId'];
+        $destLanguageId = (int)$params['destLanguageId'];
+        $params['uidList'] = array_reverse($params['uidList']);
+
+        // Build command map
+        $cmd = [
+            'tt_content' => []
+        ];
+
+        for ($i = 0, $count = count($params['uidList']); $i < $count; ++$i) {
+            $currentUid = $params['uidList'][$i];
+
+            if ($params['action'] === 'localize') {
+                if ($srcLanguageId === 0) {
+                    $cmd['tt_content'][$currentUid] = [
+                        'localize' => $destLanguageId
+                    ];
+                } else {
+                    $cmd['tt_content'][$currentUid] = [
+                        'copy' => [
+                            'action' => 'paste',
+                            'target' => $pageId,
+                            'update' => [
+                                'sys_language_uid' => $destLanguageId
+                            ]
+                        ]
+                    ];
+                }
+            } else {
+                $cmd['tt_content'][$currentUid] = [
+                    'copy' => [
+                        'action' => 'paste',
+                        'target' => $pageId,
+                        'update' => [
+                            'sys_language_uid' => $destLanguageId,
+                            'l18n_parent' => 0
+                        ]
+                    ]
+                ];
+            }
+        }
+
+        /** @var DataHandler $dataHandler */
+        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
+        $dataHandler->start([], $cmd);
+        $dataHandler->process_cmdmap();
+
+        $response->getBody()->write(json_encode([]));
+        return $response;
+    }
+
+    /**
+     * Get records for copy process
+     *
+     * @param int $pageId
+     * @param int $colPos
+     * @param int $languageId
+     * @param string $fields
+     * @return bool|\mysqli_result|object
+     */
+    protected function getRecordsToCopyDatabaseResult($pageId, $colPos, $languageId, $fields = '*')
+    {
+        return $this->getDatabaseConnection()->exec_SELECTquery(
+            $fields,
+            'tt_content',
+            'tt_content.sys_language_uid=' . (int)$languageId
+            . ' AND tt_content.colPos = ' . (int)$colPos
+            . ' AND tt_content.pid=' . (int)$pageId
+            . BackendUtility::deleteClause('tt_content')
+            . BackendUtility::versioningPlaceholderClause('tt_content'),
+            '',
+            'tt_content.sorting'
+        );
+    }
+
+    /**
+     * Returns the current BE user.
+     *
+     * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
+     */
+    protected function getBackendUser()
+    {
+        return $GLOBALS['BE_USER'];
+    }
+
+    /**
+     * Returns the database connection
+     *
+     * @return \TYPO3\CMS\Core\Database\DatabaseConnection
+     */
+    protected function getDatabaseConnection()
+    {
+        return $GLOBALS['TYPO3_DB'];
+    }
+}
\ No newline at end of file
index 1dd53ba..8bc0875 100755 (executable)
@@ -1290,44 +1290,6 @@ class PageLayoutController
     }
 
     /**
-     * Get used languages in a colPos of a page
-     *
-     * @param int $pageId
-     * @param int $colPos
-     * @return bool|\mysqli_result|object
-     */
-    public function getUsedLanguagesInPageAndColumn($pageId, $colPos)
-    {
-        if (!isset($languagesInColumnCache[$pageId])) {
-            $languagesInColumnCache[$pageId] = array();
-        }
-        if (!isset($languagesInColumnCache[$pageId][$colPos])) {
-            $languagesInColumnCache[$pageId][$colPos] = array();
-        }
-
-        if (empty($languagesInColumnCache[$pageId][$colPos])) {
-            $exQ = BackendUtility::deleteClause('tt_content') .
-                ($this->getBackendUser()->isAdmin() ? '' : ' AND sys_language.hidden=0');
-
-            $databaseConnection = $this->getDatabaseConnection();
-            $res = $databaseConnection->exec_SELECTquery(
-                'sys_language.*',
-                'tt_content,sys_language',
-                'tt_content.sys_language_uid=sys_language.uid AND tt_content.colPos = ' . (int)$colPos . ' AND tt_content.pid=' . (int)$pageId . $exQ .
-                BackendUtility::versioningPlaceholderClause('tt_content'),
-                'tt_content.sys_language_uid,sys_language.uid,sys_language.pid,sys_language.tstamp,sys_language.hidden,sys_language.title,sys_language.language_isocode,sys_language.static_lang_isocode,sys_language.flag',
-                'sys_language.title'
-            );
-            while ($row = $databaseConnection->sql_fetch_assoc($res)) {
-                $languagesInColumnCache[$pageId][$colPos][$row['uid']] = $row;
-            }
-            $databaseConnection->sql_free_result($res);
-        }
-
-        return $languagesInColumnCache[$pageId][$colPos];
-    }
-
-    /**
      * Check if a column of a page for a language is empty. Translation records are ignored here!
      *
      * @param int $colPos
index 0290a1e..de5b1e6 100644 (file)
@@ -218,7 +218,9 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
         parent::__construct();
         $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
         $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
+        $pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_layout.xlf');
         $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip');
+        $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Localization');
 
     }
 
@@ -1672,60 +1674,27 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
      */
     public function newLanguageButton($defLanguageCount, $lP, $colPos = 0)
     {
+        $lP = (int)$lP;
         if (!$this->doEdit || !$lP) {
             return '';
         }
-
-        $copyFromLanguageMenu = '';
-        foreach ($this->getLanguagesToCopyFrom(GeneralUtility::_GP('id'), $lP, $colPos) as $languageId => $label) {
-            $elementsInColumn = $languageId === 0 ? $defLanguageCount : $this->getElementsFromColumnAndLanguage(GeneralUtility::_GP('id'), $colPos, $languageId);
-            if (!empty($elementsInColumn)) {
-                $onClick = 'window.location.href=' . GeneralUtility::quoteJSvalue(BackendUtility::getLinkToDataHandlerAction('&cmd[tt_content][' . implode(',', $elementsInColumn) . '][copyFromLanguage]=' . GeneralUtility::_GP('id') . ',' . $lP)) . '; return false;';
-                $copyFromLanguageMenu .= '<li><a href="#" onclick="' . htmlspecialchars($onClick) . '">' . $this->languageFlag($languageId, false) . ' ' . htmlspecialchars($label) . '</a></li>' . LF;
-            }
-        }
-        if ($copyFromLanguageMenu !== '') {
-            $copyFromLanguageMenu =
-                '<ul class="dropdown-menu">'
-                    . $copyFromLanguageMenu
-                . '</ul>';
-        }
+        $theNewButton = '';
 
         if (!empty($defLanguageCount)) {
-            $params = '';
-            foreach ($defLanguageCount as $uidVal) {
-                $params .= '&cmd[tt_content][' . $uidVal . '][localize]=' . $lP;
-            }
-
-            // We have content in the default language, create a split button
-            $onClick = 'window.location.href=' . GeneralUtility::quoteJSvalue(BackendUtility::getLinkToDataHandlerAction($params)) . '; return false;';
             $theNewButton =
-                '<div class="btn-group">
-                    <input
-                        class="btn btn-default"
-                                               type="submit" onclick="' . htmlspecialchars($onClick) . '; return false;"
-                                               value="' . htmlspecialchars($this->getLanguageService()->getLL('newPageContent_copyForLang', true) . ' [' . count($defLanguageCount) . ']') . '"
-                                       />';
-            if ($copyFromLanguageMenu !== '' && $this->isColumnEmpty($colPos, $lP)) {
-                $theNewButton .= '<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">'
-                    . '<span class="caret"></span>'
-                    . '<span class="sr-only">Toggle Dropdown</span>'
-                    . '</button>'
-                    . $copyFromLanguageMenu;
-            }
-            $theNewButton .= '</div>';
-        } else {
-            if ($copyFromLanguageMenu !== '' && $this->isColumnEmpty($colPos, $lP)) {
-                $theNewButton =
-                    '<div class="btn-group">'
-                    . '<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">'
-                    . $this->getLanguageService()->getLL('newPageContent_copyFromAnotherLang_button', true) . ' <span class="caret"></span>'
-                    . '</button>'
-                    . $copyFromLanguageMenu
-                    . '</div>';
-            } else {
-                $theNewButton = '';
-            }
+                '<input'
+                    . ' class="btn btn-default t3js-localize"'
+                    . ' type="button"'
+                    . ' disabled'
+                    . ' value="' . htmlspecialchars($this->getLanguageService()->getLL('newPageContent_translate', true)) . '"'
+                    . ' data-has-elements="' . (int)!empty($this->contentElementCache[$lP][$colPos]) . '"'
+                    . ' 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) . '"'
+                . '/>';
         }
 
         return '<div class="t3-page-lang-copyce">' . $theNewButton . '</div>';
@@ -1776,170 +1745,6 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
     }
 
     /**
-     * Get available languages for a page
-     *
-     * @param int $pageId
-     * @return array
-     */
-    protected function getAvailableLanguages($pageId)
-    {
-        // First, select all
-        $res = $this->getPageLayoutController()->exec_languageQuery(0);
-        $langSelItems = array();
-        while ($row = $this->getDatabase()->sql_fetch_assoc($res)) {
-            if ($this->getBackendUser()->checkLanguageAccess($row['uid'])) {
-                $langSelItems[$row['uid']] = $row['title'];
-            }
-        }
-        $this->getDatabase()->sql_free_result($res);
-
-        // Remove disallowed languages
-        if (count($langSelItems) > 1
-            && !$this->getBackendUser()->user['admin']
-            && $this->getBackendUser()->groupData['allowed_languages'] !== ''
-        ) {
-            $allowed_languages = array_flip(explode(',', $this->getBackendUser()->groupData['allowed_languages']));
-            if (!empty($allowed_languages)) {
-                foreach ($langSelItems as $key => $value) {
-                    if (!isset($allowed_languages[$key]) && $key != 0) {
-                        unset($langSelItems[$key]);
-                    }
-                }
-            }
-        }
-        // Remove disabled languages
-        $modSharedTSconfig = BackendUtility::getModTSconfig($pageId, 'mod.SHARED');
-        $disableLanguages = isset($modSharedTSconfig['properties']['disableLanguages'])
-            ? GeneralUtility::trimExplode(',', $modSharedTSconfig['properties']['disableLanguages'], true)
-            : array();
-        if (!empty($langSelItems) && !empty($disableLanguages)) {
-            foreach ($disableLanguages as $language) {
-                if ($language != 0 && isset($langSelItems[$language])) {
-                    unset($langSelItems[$language]);
-                }
-            }
-        }
-
-        return $langSelItems;
-    }
-
-    /**
-     * Get available languages for copying into another language
-     *
-     * @param int $pageId
-     * @param int $excludeLanguage
-     * @param int $colPos
-     * @return array
-     */
-    protected function getLanguagesToCopyFrom($pageId, $excludeLanguage = null, $colPos = 0)
-    {
-        $langSelItems = array();
-        if (!$this->isColumnEmpty($colPos, 0)) {
-            $langSelItems[0] = $this->getLanguageService()->getLL('newPageContent_translateFromDefault', true);
-        }
-
-        $languages = $this->getUsedLanguagesInPageAndColumn($pageId, $colPos);
-        foreach ($languages as $uid => $language) {
-            $langSelItems[$uid] = sprintf($this->getLanguageService()->getLL('newPageContent_copyFromAnotherLang'), htmlspecialchars($language['title']));
-        }
-
-        if (isset($langSelItems[$excludeLanguage])) {
-            unset($langSelItems[$excludeLanguage]);
-        }
-
-        return $langSelItems;
-    }
-
-    /**
-     * Get used languages in a colPos of a page
-     *
-     * @param int $pageId
-     * @param int $colPos
-     * @return bool|\mysqli_result|object
-     */
-    protected function getUsedLanguagesInPageAndColumn($pageId, $colPos)
-    {
-        if (!isset($this->languagesInColumnCache[$pageId])) {
-            $this->languagesInColumnCache[$pageId] = array();
-        }
-        if (!isset($this->languagesInColumnCache[$pageId][$colPos])) {
-            $this->languagesInColumnCache[$pageId][$colPos] = array();
-        }
-
-        if (empty($this->languagesInColumnCache[$pageId][$colPos])) {
-            $exQ = BackendUtility::deleteClause('tt_content') .
-                ($this->getBackendUser()->isAdmin() ? '' : ' AND sys_language.hidden=0');
-
-            $databaseConnection = $this->getDatabaseConnection();
-            $res = $databaseConnection->exec_SELECTquery(
-                'sys_language.*',
-                'tt_content,sys_language',
-                'tt_content.sys_language_uid=sys_language.uid AND tt_content.colPos = ' . (int)$colPos . ' AND tt_content.pid=' . (int)$pageId . $exQ .
-                BackendUtility::versioningPlaceholderClause('tt_content'),
-                'tt_content.sys_language_uid,sys_language.uid,sys_language.pid,sys_language.tstamp,sys_language.hidden,sys_language.title,sys_language.language_isocode,sys_language.static_lang_isocode,sys_language.flag',
-                'sys_language.title'
-            );
-            while ($row = $databaseConnection->sql_fetch_assoc($res)) {
-                $this->languagesInColumnCache[$pageId][$colPos][$row['uid']] = $row;
-            }
-            $databaseConnection->sql_free_result($res);
-        }
-
-        return $this->languagesInColumnCache[$pageId][$colPos];
-    }
-
-    /**
-     * Check if a column of a page for a language is empty. Translation records are ignored here!
-     *
-     * @param int $colPos
-     * @param int $languageId
-     * @return bool
-     */
-    protected function isColumnEmpty($colPos, $languageId)
-    {
-        foreach ($this->contentElementCache[$languageId][$colPos] as $uid => $row) {
-            if ((int)$row['l18n_parent'] === 0) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Get elements for a column and a language
-     *
-     * @param int $pageId
-     * @param int $colPos
-     * @param int $languageId
-     * @return array
-     */
-    protected function getElementsFromColumnAndLanguage($pageId, $colPos, $languageId)
-    {
-        if (!isset($this->contentElementCache[$languageId][$colPos])) {
-            $languageId = (int)$languageId;
-            $whereClause = 'tt_content.pid=' . (int)$pageId . ' AND tt_content.colPos=' . (int)$colPos . ' AND tt_content.sys_language_uid=' . $languageId . BackendUtility::deleteClause('tt_content');
-            if ($languageId > 0) {
-                $whereClause .= ' AND tt_content.l18n_parent=0 AND sys_language.uid=' . $languageId . ($this->getBackendUser()->isAdmin() ? '' : ' AND sys_language.hidden=0');
-            }
-
-            $databaseConnection = $this->getDatabaseConnection();
-            $res = $databaseConnection->exec_SELECTquery(
-                'tt_content.uid',
-                'tt_content,sys_language',
-                $whereClause
-            );
-            while ($row = $databaseConnection->sql_fetch_assoc($res)) {
-                $this->contentElementCache[$languageId][$colPos][$row['uid']] = $row;
-            }
-            $databaseConnection->sql_free_result($res);
-        }
-        if (is_array($this->contentElementCache[$languageId][$colPos])) {
-            return array_keys($this->contentElementCache[$languageId][$colPos]);
-        }
-        return array();
-    }
-
-    /**
      * Make selector box for creating new translation in a language
      * Displays only languages which are not yet present for the current page and
      * that are not disabled with page TS.
index 3ba27a8..4e620a7 100644 (file)
@@ -199,4 +199,22 @@ return [
         'path' => '/link-browser/encode-typolink',
         'target' => \TYPO3\CMS\Backend\Controller\LinkBrowserController::class . '::encodeTypoLink',
     ],
+
+    // Get languages in page and colPos
+    'languages_page_colpos' => [
+        'path' => '/records/localize/get-languages',
+        'target' => Controller\Page\LocalizationController::class . '::getUsedLanguagesInPageAndColumn'
+    ],
+
+    // Get summary of records to localize
+    'records_localize_summary' => [
+        'path' => '/records/localize/summary',
+        'target' => Controller\Page\LocalizationController::class . '::getRecordLocalizeSummary'
+    ],
+
+    // Localize the records
+    'records_localize' => [
+        'path' => '/records/localize',
+        'target' => Controller\Page\LocalizationController::class . '::localizeRecords'
+    ]
 ];
index 6cd35c6..38bdbed 100644 (file)
@@ -66,6 +66,9 @@
                        <trans-unit id="newPageContent">
                                <source>Create page content</source>
                        </trans-unit>
+                       <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="staleTranslationWarningTitle">
                                <source>Inconsistent content detected in language "%s"</source>
                        </trans-unit>
+                       <trans-unit id="localize.chooseLanguage">
+                               <source>Choose your language</source>
+                       </trans-unit>
+                       <trans-unit id="localize.wizard.header">
+                               <source>Localize record &quot;{0}&quot; into {1}</source>
+                       </trans-unit>
+                       <trans-unit id="localize.wizard.button.cancel">
+                               <source>Cancel</source>
+                       </trans-unit>
+                       <trans-unit id="localize.wizard.button.next">
+                               <source>Next</source>
+                       </trans-unit>
+                       <trans-unit id="localize.wizard.button.process">
+                               <source>Start processing</source>
+                       </trans-unit>
+                       <trans-unit id="localize.view.chooseLanguage">
+                               <source>Choose the language from which you want to localize the content</source>
+                       </trans-unit>
+                       <trans-unit id="localize.view.summary">
+                               <source>Record summary</source>
+                       </trans-unit>
+                       <trans-unit id="localize.view.processing">
+                               <source>Processing...</source>
+                       </trans-unit>
+                       <trans-unit id="localize.progress.step">
+                               <source>Step {0} of {1}</source>
+                       </trans-unit>
+                       <trans-unit id="localize.educate.translate" xml:space="preserve">
+                               <source>Translating content will create a direct connection between the original language and the language you translate to.&lt;br&gt;
+                                       This means that moving and element or setting meta information like start- or endtime will be taken from the original content and you will not be able to set these values on a translated content element.&lt;br&gt;
+                                       &lt;strong&gt;Use this when your workflow demands a strict translation workflow.&lt;/strong&gt;
+                               </source>
+                       </trans-unit>
+                       <trans-unit id="localize.educate.copy" xml:space="preserve">
+                               <source>Copying content will take the content elements from the source language and create copies in a different language.&lt;br&gt;
+                                       This means that you will be able to move content elements around freely, but you will not have the benefit of being able to compare changes made in the source language later on.&lt;br&gt;
+                                       &lt;strong&gt;Use this when you want to have freedom in designing your translated website.&lt;/strong&gt;
+                               </source>
+                       </trans-unit>
                </body>
        </file>
 </xliff>
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Localization.js b/typo3/sysext/backend/Resources/Public/JavaScript/Localization.js
new file mode 100644 (file)
index 0000000..0c03d39
--- /dev/null
@@ -0,0 +1,372 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Module: TYPO3/CMS/Backend/Localization
+ * UI for localization workflow.
+ */
+define([
+       'jquery',
+       'TYPO3/CMS/Backend/AjaxDataHandler',
+       'TYPO3/CMS/Backend/Modal',
+       'TYPO3/CMS/Backend/Icons',
+       'bootstrap'
+], function($, DataHandler, Modal, Icons) {
+       'use strict';
+
+       /**
+        * @type {{identifier: {triggerButton: string}, actions: {translate: $, copy: $}, settings: {}, records: []}}
+        * @exports TYPO3/CMS/Backend/Localization
+        */
+       var Localization = {
+               identifier: {
+                       triggerButton: '.t3js-localize'
+               },
+               actions: {
+                       translate: $('<label />', {
+                               class: 'btn btn-block btn-default t3js-option',
+                               'data-helptext': '.t3js-helptext-translate'
+                       }).html('<br>Translate').prepend(
+                               $('<input />', {
+                                       type: 'radio',
+                                       name: 'mode',
+                                       id: 'mode_translate',
+                                       value: 'localize',
+                                       style: 'display: none'
+                               })
+                       ),
+                       copy: $('<label />', {
+                               class: 'btn btn-block btn-default t3js-option',
+                               'data-helptext': '.t3js-helptext-copy'
+                       }).html('<br>Copy').prepend(
+                               $('<input />', {
+                                       type: 'radio',
+                                       name: 'mode',
+                                       id: 'mode_copy',
+                                       value: 'copyFromLanguage',
+                                       style: 'display: none'
+                               })
+                       )
+               },
+               settings: {},
+               records: []
+       };
+
+       Localization.initialize = function() {
+               $.when(
+                       Icons.getIcon('actions-localize', Icons.sizes.large),
+                       Icons.getIcon('actions-edit-copy', Icons.sizes.large)
+               ).done(function(localizeIconMarkup, copyIconMarkup) {
+                       Localization.actions.translate.prepend(localizeIconMarkup[0]);
+                       Localization.actions.copy.prepend(copyIconMarkup[0]);
+                       $(Localization.identifier.triggerButton).prop('disabled', false);
+               });
+
+               $(document).on('click', Localization.identifier.triggerButton, function() {
+                       var $triggerButton = $(this),
+                               modalContent =
+                                       '<div id="localization-carousel" class="carousel slide" data-ride="carousel" data-interval="false">'
+                                               + '<div class="carousel-inner" role="listbox">'
+                                                       + '<div class="item active">'
+                                                               + '<div data-toggle="buttons">'
+                                                                       + '<div class="row">'
+                                                                               + '<div class="btn-group col-sm-3">' + Localization.actions.translate[0].outerHTML + '</div>'
+                                                                               + '<div class="col-sm-9">'
+                                                                                       + '<p class="t3js-helptext t3js-helptext-translate text-muted">' + TYPO3.lang['localize.educate.translate'] + '</p>'
+                                                                               + '</div>'
+                                                                       + '</div>';
+
+                       if ($triggerButton.data('hasElements') === 0) {
+                               modalContent +=
+                                       '<hr>'
+                                       + '<div class="row">'
+                                               + '<div class="col-sm-3 btn-group">' + Localization.actions.copy[0].outerHTML + '</div>'
+                                               + '<div class="col-sm-9">'
+                                                       + '<p class="t3js-helptext t3js-helptext-copy text-muted">' + TYPO3.lang['localize.educate.copy'] + '</p>'
+                                               + '</div>'
+                                       + '</div>';
+                       }
+
+                       modalContent +=         '</div>'
+                                                       + '</div>'
+                                                       + '<div class="item">'
+                                                               + '<h4>' + TYPO3.lang['localize.view.chooseLanguage'] + '</h4>'
+                                                               + '<div class="t3js-available-languages">'
+                                                               + '</div>'
+                                                       + '</div>'
+                                                       + '<div class="item">'
+                                                               + '<h4>' + TYPO3.lang['localize.view.summary'] + '</h4>'
+                                                               + '<div class="t3js-summary">'
+                                                               + '</div>'
+                                                       + '</div>'
+                                                       + '<div class="item">'
+                                                               + '<h4>' + TYPO3.lang['localize.view.processing'] + '</h4>'
+                                                               + '<div class="t3js-processing">'
+                                                               + '</div>'
+                                                       + '</div>'
+                                               + '</div>'
+                                       + '</div>';
+
+                       var $modal = Modal.confirm(
+                               TYPO3.lang['localize.wizard.header'].replace('{0}', $triggerButton.data('colposName')).replace('{1}', $triggerButton.data('languageName')),
+                               modalContent,
+                               top.TYPO3.Severity.info, [
+                                       {
+                                               text: TYPO3.lang['localize.wizard.button.cancel'] || 'Cancel',
+                                               active: true,
+                                               btnClass: 'btn-default',
+                                               name: 'cancel',
+                                               trigger: function() {
+                                                       Modal.currentModal.trigger('modal-dismiss');
+                                               }
+                                       }, {
+                                               text: TYPO3.lang['localize.wizard.button.next'] || 'Next',
+                                               btnClass: 'btn-info',
+                                               name: 'next'
+                                       }
+                               ], [
+                                       'localization-wizard'
+                               ]
+                       );
+
+                       var $carousel = $modal.find('#localization-carousel'),
+                               slideCount = Math.max(1, $modal.find('#localization-carousel .item').length),
+                               initialStep = Math.round(100 / slideCount),
+                               $modalFooter = $modal.find('.modal-footer'),
+                               $nextButton = $modalFooter.find('button[name="next"]');
+
+                       $carousel.data('slideCount', slideCount);
+                       $carousel.data('currentSlide', 1);
+
+                       // Append progress bar to modal footer
+                       $modalFooter.prepend(
+                               $('<div />', {class: 'progress'}).append(
+                                       $('<div />', {
+                                               role: 'progressbar',
+                                               class: 'progress-bar',
+                                               'aria-valuemin': 0,
+                                               'aria-valuenow': initialStep,
+                                               'aria-valuemax': 100
+                                       }).width(initialStep + '%').text(TYPO3.lang['localize.progress.step'].replace('{0}', '1').replace('{1}', slideCount))
+                               )
+                       );
+
+                       // Disable "next" button on initialization and bind "click" event
+                       $nextButton.prop('disabled', true).on('click', function() {
+                               Localization.synchronizeSlidesHeight($carousel);
+                               $carousel.carousel('next');
+                       });
+
+                       // Register "click" event on options
+                       $modal.on('click', '.t3js-option', function() {
+                               var $me = $(this),
+                                       $radio = $me.find('input[type="radio"]');
+
+                               if ($me.data('helptext')) {
+                                       $modal.find('.t3js-helptext').addClass('text-muted');
+                                       $modal.find($me.data('helptext')).removeClass('text-muted');
+                               }
+                               if ($radio.length > 0) {
+                                       Localization.settings[$radio.attr('name')] = $radio.val();
+                               }
+                               $nextButton.prop('disabled', false);
+                       });
+
+                       $carousel.on('slide.bs.carousel', function(e) {
+                               var nextSlideNumber = $carousel.data('currentSlide') + 1,
+                                       $modalFooter = $carousel.parent().next();
+
+                               $carousel.data('currentSlide', nextSlideNumber);
+                               $modalFooter.find('.progress-bar')
+                                       .width(initialStep * nextSlideNumber + '%')
+                                       .text(TYPO3.lang['localize.progress.step'].replace('{0}', nextSlideNumber).replace('{1}', slideCount));
+
+                               // Disable next button again
+                               $nextButton.prop('disabled', true);
+                       }).on('slid.bs.carousel', function(e) {
+                               var $activeSlide = $(e.relatedTarget),
+                                       $modalFooter = $carousel.parent().next(),
+                                       $languageView = $activeSlide.find('.t3js-available-languages'),
+                                       $summaryView = $activeSlide.find('.t3js-summary'),
+                                       $processingView = $activeSlide.find('.t3js-processing');
+
+                               if ($languageView.length > 0) {
+                                       // Prepare language view
+                                       Icons.getIcon('spinner-circle-dark', Icons.sizes.large).done(function(markup) {
+                                               $languageView.html(
+                                                       $('<div />', {class: 'text-center'}).append(markup)
+                                               );
+                                               Localization.loadAvailableLanguages(
+                                                       $triggerButton.data('pageId'),
+                                                       $triggerButton.data('colposId'),
+                                                       $triggerButton.data('languageId')
+                                               ).done(function(result) {
+                                                       if (result.length === 1) {
+                                                               // We only have one result, auto select the record and continue
+                                                               Localization.settings.language = result[0].uid + ''; // we need a string
+                                                               $carousel.carousel('next');
+                                                               return;
+                                                       }
+
+                                                       var $languageButtons = $('<div />', {class: 'row', 'data-toggle': 'buttons'});
+
+                                                       $.each(result, function(_, languageObject) {
+                                                               $languageButtons.append(
+                                                                       $('<div />', {class: 'col-sm-4'}).append(
+                                                                               $('<label />', {class: 'btn btn-default btn-block t3js-option option'}).text(' ' + languageObject.title).prepend(
+                                                                                       languageObject.flagIcon
+                                                                               ).prepend(
+                                                                                       $('<input />', {
+                                                                                               type: 'radio',
+                                                                                               name: 'language',
+                                                                                               id: 'language' + languageObject.uid,
+                                                                                               value: languageObject.uid,
+                                                                                               style: 'display: none;'
+                                                                                       })
+                                                                               )
+                                                                       )
+                                                               );
+                                                       });
+                                                       $languageView.html($languageButtons);
+                                               });
+                                       });
+                               } else if ($summaryView.length > 0) {
+                                       Icons.getIcon('spinner-circle-dark', Icons.sizes.large).done(function(markup) {
+                                               $summaryView.html(
+                                                       $('<div />', {class: 'text-center'}).append(markup)
+                                               );
+
+                                               Localization.getSummary(
+                                                       $triggerButton.data('pageId'),
+                                                       $triggerButton.data('colposId')
+                                               ).done(function(result) {
+                                                       var $summary = $('<div />', {class: 'row'});
+                                                       Localization.records = [];
+
+                                                       $.each(result, function(_, record) {
+                                                               Localization.records.push(record.uid);
+                                                               $summary.append(
+                                                                       $('<div />', {class: 'col-sm-6'}).text(' (' + record.uid + ') ' + record.title).prepend(record.icon)
+                                                               );
+                                                       });
+                                                       $summaryView.html($summary);
+
+                                                       // Unlock button as we don't have an option
+                                                       $nextButton.prop('disabled', false);
+                                                       $nextButton.text(TYPO3.lang['localize.wizard.button.process'])
+                                               });
+                                       });
+                               } else if ($processingView.length > 0) {
+                                       // Point of no return - hide modal footer disable any closing ability
+                                       $modal.find('.modal-header .close').remove();
+                                       $modalFooter.slideUp();
+
+                                       Icons.getIcon('spinner-circle-dark', Icons.sizes.large).done(function(markup) {
+                                               $processingView.html(
+                                                       $('<div />', {class: 'text-center'}).append(markup)
+                                               );
+
+                                               Localization.localizeRecords(
+                                                       $triggerButton.data('pageId'),
+                                                       $triggerButton.data('languageId'),
+                                                       Localization.records
+                                               ).done(function() {
+                                                       Modal.dismiss();
+                                                       document.location.reload();
+                                               });
+                                       });
+                               }
+                       });
+               });
+
+               /**
+                * Synchronize height of slides
+                *
+                * @param {$} $carousel
+                */
+               Localization.synchronizeSlidesHeight = function($carousel) {
+                       var $slides = $carousel.find('.item'),
+                               maxHeight = 0;
+
+                       $slides.each(function(_, slide) {
+                               var height = $(slide).height();
+                               if (height > maxHeight) {
+                                       maxHeight = height;
+                               }
+                       });
+                       $slides.height(maxHeight);
+               };
+
+               /**
+                * Load available languages from page and colPos
+                *
+                * @param {Integer} pageId
+                * @param {Integer} colPos
+                * @param {Integer} languageId
+                * @return {Promise}
+                */
+               Localization.loadAvailableLanguages = function(pageId, colPos, languageId) {
+                       return $.ajax({
+                               url: TYPO3.settings.ajaxUrls['languages_page_colpos'],
+                               data: {
+                                       pageId: pageId,
+                                       colPos: colPos,
+                                       languageId: languageId
+                               }
+                       });
+               };
+
+               /**
+                * Get summary for record processing
+                *
+                * @param {Integer} pageId
+                * @param {Integer} colPos
+                * @return {Promise}
+                */
+               Localization.getSummary = function(pageId, colPos) {
+                       return $.ajax({
+                               url: TYPO3.settings.ajaxUrls['records_localize_summary'],
+                               data: {
+                                       pageId: pageId,
+                                       colPos: colPos,
+                                       languageId: Localization.settings.language
+                               }
+                       });
+               };
+
+               /**
+                * Localize records
+                *
+                * @param {Integer} pageId
+                * @param {Integer} languageId
+                * @param {Array} uidList
+                * @return {Promise}
+                */
+               Localization.localizeRecords = function(pageId, languageId, uidList) {
+                       return $.ajax({
+                               url: TYPO3.settings.ajaxUrls['records_localize'],
+                               data: {
+                                       pageId: pageId,
+                                       srcLanguageId: Localization.settings.language,
+                                       destLanguageId: languageId,
+                                       action: Localization.settings.mode,
+                                       uidList: uidList
+                               }
+                       });
+               };
+       };
+
+       $(Localization.initialize);
+
+       return Localization;
+});
\ No newline at end of file
index 86e3f98..be55372 100644 (file)
@@ -3318,9 +3318,6 @@ class DataHandler
                             case 'inlineLocalizeSynchronize':
                                 $this->inlineLocalizeSynchronize($table, $id, $value);
                                 break;
-                            case 'copyFromLanguage':
-                                $this->copyRecordFromLanguage($table, $id, $value);
-                                break;
                             case 'delete':
                                 $this->deleteAction($table, $id);
                                 break;
@@ -4745,28 +4742,6 @@ class DataHandler
         }
     }
 
-    /**
-     * Creates a independent copy of content elements into another language.
-     *
-     * @param string $table The table of the localized parent record
-     * @param string $id Comma separated list of content element ids
-     * @param string $value Comma separated list of the destination and the target language
-     * @return void
-     */
-    protected function copyRecordFromLanguage($table, $id, $value)
-    {
-        list($destination, $language) = GeneralUtility::intExplode(',', $value);
-
-        // array_reverse is required to keep the order of elements
-        $idList = array_reverse(GeneralUtility::intExplode(',', $id, true));
-        foreach ($idList as $contentElementUid) {
-            $this->copyRecord($table, $contentElementUid, $destination, true, array(
-                $GLOBALS['TCA'][$table]['ctrl']['languageField'] => $language,
-                $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] => 0
-            ), '', 0, true);
-        }
-    }
-
     /*********************************************
      *
      * Cmd: Deleting
index 168f71d..d332d3c 100644 (file)
@@ -13951,6 +13951,9 @@ div.typo3-module-lang div.progress-bar {
   max-width: 28px;
   max-height: 28px;
 }
+.localization-wizard .option {
+  margin-bottom: 4px;
+}
 .workspace-info {
   padding: 7px;
 }