[FEATURE] Introduce allowLanguageSynchronization 91/51291/31
authorOliver Hader <oliver@typo3.org>
Thu, 12 Jan 2017 14:21:52 +0000 (15:21 +0100)
committerChristian Kuhn <lolli@schwarzbu.ch>
Tue, 7 Feb 2017 16:09:10 +0000 (17:09 +0100)
This feature introduces a new functionality called
"allowLanguageSynchronization" which can be set on a field
configuration of a TCA column. This is the successor of
"l10n_mode=mergeIfNotBlank" as the old option had several
conceptual downsides:

1) "mergeIfNotBlank" took the value of the default record
   during runtime, but only if the translation field was empty.
   This means it was not possible to see what the record
   actually contained without having all fields of the parent
   at hand.

2) It was not possible to have a value "santa" in the original
   record but remove the option in a translation (because an
   empty string "" implicitly triggered the runtime call in the
   frontend)

3) "mergeIfNotBlank" did not work on relations except for files
   fetched via the FileRepository API calls, but for no other
   inline elements.

4) "mergeIfNotBlank" did the overlay functionality in the frontend,
   but only FormEngine and DataHandler took care of the option.
   Custom backend modules had to implement the same functionality.

5) In FormEngine, there was an icon in the translation record that
   if the record kept empty the value of the original language was
   taken, but this is not optimal in terms of usability.

6) "mergeIfNotBlank" did not take the new l10n_source option into
   account, where localizations could be made from other records
   than the default language "0".

The new feature can be set on any TCA column setting:

$GLOBALS['TCA'][<table-name>]['columns']
[<field-name>]['config']['behaviour']
['allowLanguageSynchronization'] = true;

This brings an option to records with translations (both from
l10n_parent and l10n_source) to have the value for all translations
synchronized or explictly have a checkbox to use a custom value.

The information whether a field is custom filled, or kept in sync
from l10n_parent/l10n_source is stored in a separate field called
"l10n_state" inside the database.

The introduced upgrade wizard and TCA migration to remove
"l10n_mode=mergeIfNotBlank" has been modified to migrate to this
option and add a l10n_state database field if a TCA table used
"mergeIfNotBlank" but did not add the l10n_state field manually
via ext_tables.sql yet.

New extensions can easily use the new option right away,
extensions that need to stay compatible with v7 and v8 can add
both options right away to have the same output.

The main goals to achieve with this change is now:

* Have consistent database values for all records regardless
  of l10n_mode=mergeIfNotBlank paving the way to fetch translated
  records without having to overlay (once l10n_mode=exclude is
  also copying values and relations)
* Be more explicit for editors about records that have a different
  or the same state as their l10n_parent/l10n_source as a benefit
  for bigger instances with a lot of languages
* Avoid hidden magic when retrieving localized records in the
  TYPO3 Frontend.

Resolves: #79658
Related: #79243
Releases: master
Change-Id: I6c2dbfeb09b47f958a536c9ab050c24ba4bbcbbd
Reviewed-on: https://review.typo3.org/51291
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Frans Saris <franssaris@gmail.com>
Tested-by: Frans Saris <franssaris@gmail.com>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
53 files changed:
typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php
typo3/sysext/backend/Classes/Form/Element/CheckboxElement.php
typo3/sysext/backend/Classes/Form/Element/GroupElement.php
typo3/sysext/backend/Classes/Form/Element/ImageManipulationElement.php
typo3/sysext/backend/Classes/Form/Element/InputColorPickerElement.php
typo3/sysext/backend/Classes/Form/Element/InputDateTimeElement.php
typo3/sysext/backend/Classes/Form/Element/InputLinkElement.php
typo3/sysext/backend/Classes/Form/Element/InputTextElement.php
typo3/sysext/backend/Classes/Form/Element/RadioElement.php
typo3/sysext/backend/Classes/Form/Element/SelectCheckBoxElement.php
typo3/sysext/backend/Classes/Form/Element/SelectMultipleSideBySideElement.php
typo3/sysext/backend/Classes/Form/Element/SelectSingleBoxElement.php
typo3/sysext/backend/Classes/Form/Element/SelectSingleElement.php
typo3/sysext/backend/Classes/Form/Element/TextElement.php
typo3/sysext/backend/Classes/Form/Element/TextTableElement.php
typo3/sysext/backend/Classes/Form/FieldWizard/LocalizationStateSelector.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FormDataCompiler.php
typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseLanguageRows.php
typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseRecordTypeValue.php
typo3/sysext/backend/Classes/Form/NodeFactory.php
typo3/sysext/backend/Resources/Public/JavaScript/FormEngine.js
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseLanguageRowsTest.php
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseRecordTypeValueTest.php
typo3/sysext/core/Classes/DataHandling/DataHandler.php
typo3/sysext/core/Classes/DataHandling/DatabaseSchemaService.php [new file with mode: 0644]
typo3/sysext/core/Classes/DataHandling/Localization/DataMapItem.php [new file with mode: 0644]
typo3/sysext/core/Classes/DataHandling/Localization/DataMapProcessor.php [new file with mode: 0644]
typo3/sysext/core/Classes/DataHandling/Localization/State.php [new file with mode: 0644]
typo3/sysext/core/Classes/Migrations/TcaMigration.php
typo3/sysext/core/Configuration/TCA/sys_category.php
typo3/sysext/core/Documentation/Changelog/master/Deprecation-51291-PageRepositoryShouldFieldBeOverlaid.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-51291-SynchronizedFieldValuesInLocalizedRecords.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/DataHandling/Group/AbstractActionTestCase.php
typo3/sysext/core/Tests/Functional/DataHandling/Group/Modify/ActionTest.php
typo3/sysext/core/Tests/Functional/DataHandling/Group/Modify/DataSet/localizeContentOfRelationWSynchronization.csv [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/DataHandling/IRRE/CSV/AbstractActionTestCase.php
typo3/sysext/core/Tests/Functional/DataHandling/IRRE/CSV/Modify/ActionTest.php
typo3/sysext/core/Tests/Functional/DataHandling/IRRE/CSV/Modify/DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/AbstractActionTestCase.php
typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/ActionTest.php
typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/DataHandling/Regular/AbstractActionTestCase.php
typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/ActionTest.php
typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/localizeContentWSynchronization.csv [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Migrations/TcaMigrationTest.php
typo3/sysext/core/ext_localconf.php
typo3/sysext/filemetadata/Configuration/TCA/Overrides/sys_file_metadata.php
typo3/sysext/frontend/Classes/Page/PageRepository.php
typo3/sysext/frontend/Configuration/TCA/tt_content.php
typo3/sysext/frontend/Tests/Unit/Page/PageRepositoryTest.php
typo3/sysext/install/Classes/Updates/DatabaseRowsUpdateWizard.php
typo3/sysext/install/Classes/Updates/RowUpdater/L10nModeUpdater.php
typo3/sysext/lang/Resources/Private/Language/locallang_wizards.xlf

index b4e40b3..d684a70 100644 (file)
@@ -62,6 +62,15 @@ class InlineControlContainer extends AbstractContainer
     protected $requireJsModules = [];
 
     /**
+     * @var array Default wizards
+     */
+    protected $defaultFieldWizard = [
+        'localizationStateSelector' => [
+            'renderType' => 'localizationStateSelector',
+        ],
+    ];
+
+    /**
      * Container objects give $nodeFactory down to other containers.
      *
      * @param NodeFactory $nodeFactory
@@ -295,6 +304,11 @@ class InlineControlContainer extends AbstractContainer
 
         $html .= '</div>';
 
+        $fieldWizardResult = $this->renderfieldWizard();
+        $fieldWizardHtml = $fieldWizardResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
+        $html .= $fieldWizardHtml;
+
         // Add the level links after all child records:
         if ($config['appearance']['levelLinksPosition'] ===  'both' || $config['appearance']['levelLinksPosition'] === 'bottom') {
             $html .= $levelLinks . $localizationLinks;
index 913ef29..b697f15 100644 (file)
@@ -27,8 +27,14 @@ class CheckboxElement extends AbstractFormElement
      * @var array
      */
     protected $defaultFieldWizard = [
+        'localizationStateSelector' => [
+            'renderType' => 'localizationStateSelector',
+        ],
         'otherLanguageContent' => [
             'renderType' => 'otherLanguageContent',
+            'after' => [
+                'localizationStateSelector'
+            ],
         ],
         'defaultLanguageDifferences' => [
             'renderType' => 'defaultLanguageDifferences',
index b5af2d5..846396e 100644 (file)
@@ -82,9 +82,13 @@ class GroupElement extends AbstractFormElement
             'renderType' => 'fileUpload',
             'after' => [ 'recordsOverview' ],
         ],
+        'localizationStateSelector' => [
+            'renderType' => 'localizationStateSelector',
+            'after' => [ 'fileUpload' ],
+        ],
         'otherLanguageContent' => [
             'renderType' => 'otherLanguageContent',
-            'after' => [ 'fileUpload' ],
+            'after' => [ 'localizationStateSelector' ],
         ],
         'defaultLanguageDifferences' => [
             'renderType' => 'defaultLanguageDifferences',
index ea07e6e..bcf866a 100644 (file)
@@ -34,8 +34,14 @@ class ImageManipulationElement extends AbstractFormElement
      * @var array
      */
     protected $defaultFieldWizard = [
+        'localizationStateSelector' => [
+            'renderType' => 'localizationStateSelector',
+        ],
         'otherLanguageContent' => [
             'renderType' => 'otherLanguageContent',
+            'after' => [
+                'localizationStateSelector'
+            ],
         ],
         'defaultLanguageDifferences' => [
             'renderType' => 'defaultLanguageDifferences',
index f7c83a8..eb7c8b6 100644 (file)
@@ -30,8 +30,14 @@ class InputColorPickerElement extends AbstractFormElement
      * @var array
      */
     protected $defaultFieldWizard = [
+        'localizationStateSelector' => [
+            'renderType' => 'localizationStateSelector',
+        ],
         'otherLanguageContent' => [
             'renderType' => 'otherLanguageContent',
+            'after' => [
+                'localizationStateSelector'
+            ],
         ],
         'defaultLanguageDifferences' => [
             'renderType' => 'defaultLanguageDifferences',
index 2002f1b..6b291bd 100644 (file)
@@ -31,8 +31,14 @@ class InputDateTimeElement extends AbstractFormElement
      * @var array
      */
     protected $defaultFieldWizard = [
+        'localizationStateSelector' => [
+            'renderType' => 'localizationStateSelector',
+        ],
         'otherLanguageContent' => [
             'renderType' => 'otherLanguageContent',
+            'after' => [
+                'localizationStateSelector'
+            ],
         ],
         'defaultLanguageDifferences' => [
             'renderType' => 'defaultLanguageDifferences',
index e66bca8..f67b5b7 100644 (file)
@@ -52,13 +52,19 @@ class InputLinkElement extends AbstractFormElement
      * @var array
      */
     protected $defaultFieldWizard = [
-        OtherLanguageContent::class => [
+        'localizationStateSelector' => [
+            'renderType' => 'localizationStateSelector',
+        ],
+        'otherLanguageContent' => [
             'renderType' => 'otherLanguageContent',
+            'after' => [
+                'localizationStateSelector'
+            ],
         ],
-        DefaultLanguageDifferences::class => [
+        'defaultLanguageDifferences' => [
             'renderType' => 'defaultLanguageDifferences',
             'after' => [
-                OtherLanguageContent::class
+                'otherLanguageContent',
             ],
         ],
     ];
index c147ad6..08badc3 100644 (file)
@@ -33,8 +33,14 @@ class InputTextElement extends AbstractFormElement
      * @var array
      */
     protected $defaultFieldWizard = [
+        'localizationStateSelector' => [
+            'renderType' => 'localizationStateSelector',
+        ],
         'otherLanguageContent' => [
             'renderType' => 'otherLanguageContent',
+            'after' => [
+                'localizationStateSelector'
+            ],
         ],
         'defaultLanguageDifferences' => [
             'renderType' => 'defaultLanguageDifferences',
index 3d75f71..2eb27c8 100644 (file)
@@ -25,8 +25,14 @@ class RadioElement extends AbstractFormElement
      * @var array
      */
     protected $defaultFieldWizard = [
+        'localizationStateSelector' => [
+            'renderType' => 'localizationStateSelector',
+        ],
         'otherLanguageContent' => [
             'renderType' => 'otherLanguageContent',
+            'after' => [
+                'localizationStateSelector'
+            ],
         ],
         'defaultLanguageDifferences' => [
             'renderType' => 'defaultLanguageDifferences',
index 64838ea..48c0825 100644 (file)
@@ -33,8 +33,14 @@ class SelectCheckBoxElement extends AbstractFormElement
      * @var array
      */
     protected $defaultFieldWizard = [
+        'localizationStateSelector' => [
+            'renderType' => 'localizationStateSelector',
+        ],
         'otherLanguageContent' => [
             'renderType' => 'otherLanguageContent',
+            'after' => [
+                'localizationStateSelector'
+            ],
         ],
         'defaultLanguageDifferences' => [
             'renderType' => 'defaultLanguageDifferences',
index 0954b77..9cfae06 100644 (file)
@@ -55,8 +55,14 @@ class SelectMultipleSideBySideElement extends AbstractFormElement
      * @var array
      */
     protected $defaultFieldWizard = [
+        'localizationStateSelector' => [
+            'renderType' => 'localizationStateSelector',
+        ],
         'otherLanguageContent' => [
             'renderType' => 'otherLanguageContent',
+            'after' => [
+                'localizationStateSelector'
+            ],
         ],
         'defaultLanguageDifferences' => [
             'renderType' => 'defaultLanguageDifferences',
index 94a6f69..005f83d 100644 (file)
@@ -42,8 +42,14 @@ class SelectSingleBoxElement extends AbstractFormElement
      * @var array
      */
     protected $defaultFieldWizard = [
+        'localizationStateSelector' => [
+            'renderType' => 'localizationStateSelector',
+        ],
         'otherLanguageContent' => [
             'renderType' => 'otherLanguageContent',
+            'after' => [
+                'localizationStateSelector'
+            ],
         ],
         'defaultLanguageDifferences' => [
             'renderType' => 'defaultLanguageDifferences',
index 6539009..63319c7 100644 (file)
@@ -37,9 +37,15 @@ class SelectSingleElement extends AbstractFormElement
             'renderType' => 'selectIcons',
             'disabled' => true,
         ],
+        'localizationStateSelector' => [
+            'renderType' => 'localizationStateSelector',
+            'after' => [
+                'selectIcons',
+            ],
+        ],
         'otherLanguageContent' => [
             'renderType' => 'otherLanguageContent',
-            'after' => [ 'selectIcons' ],
+            'after' => [ 'localizationStateSelector' ],
         ],
         'defaultLanguageDifferences' => [
             'renderType' => 'defaultLanguageDifferences',
index 44093fe..a7d20fc 100644 (file)
@@ -31,8 +31,14 @@ class TextElement extends AbstractFormElement
      * @var array
      */
     protected $defaultFieldWizard = [
+        'localizationStateSelector' => [
+            'renderType' => 'localizationStateSelector',
+        ],
         'otherLanguageContent' => [
             'renderType' => 'otherLanguageContent',
+            'after' => [
+                'localizationStateSelector'
+            ],
         ],
         'defaultLanguageDifferences' => [
             'renderType' => 'defaultLanguageDifferences',
index c3a0250..d6d1c29 100644 (file)
@@ -30,8 +30,14 @@ class TextTableElement extends AbstractFormElement
      * @var array
      */
     protected $defaultFieldWizard = [
+        'localizationStateSelector' => [
+            'renderType' => 'localizationStateSelector',
+        ],
         'otherLanguageContent' => [
             'renderType' => 'otherLanguageContent',
+            'after' => [
+                'localizationStateSelector'
+            ],
         ],
         'defaultLanguageDifferences' => [
             'renderType' => 'defaultLanguageDifferences',
diff --git a/typo3/sysext/backend/Classes/Form/FieldWizard/LocalizationStateSelector.php b/typo3/sysext/backend/Classes/Form/FieldWizard/LocalizationStateSelector.php
new file mode 100644 (file)
index 0000000..a7da8bd
--- /dev/null
@@ -0,0 +1,141 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Backend\Form\FieldWizard;
+
+/*
+ * 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 TYPO3\CMS\Backend\Form\AbstractNode;
+use TYPO3\CMS\Core\DataHandling\Localization\State;
+use TYPO3\CMS\Lang\LanguageService;
+
+/**
+ * Allows to define the localization state per field.
+ */
+class LocalizationStateSelector extends AbstractNode
+{
+    /**
+     * Render the radio buttons if enabled
+     *
+     * @return array Result array
+     */
+    public function render(): array
+    {
+        $languageService = $this->getLanguageService();
+        $result = $this->initializeResultArray();
+
+        $fieldName = $this->data['fieldName'];
+        $l10nStateFieldName = '';
+        if (isset($l10nStateFieldName)) {
+            $l10nStateFieldName = 'l10n_state';
+        }
+        if (
+            !$l10nStateFieldName
+            || !isset($this->data['defaultLanguageRow'])
+            || !isset($this->data['processedTca']['columns'][$fieldName]['config']['behaviour']['allowLanguageSynchronization'])
+            || !$this->data['processedTca']['columns'][$fieldName]['config']['behaviour']['allowLanguageSynchronization']
+        ) {
+            return $result;
+        }
+
+        $l10nParentFieldName = $this->data['processedTca']['ctrl']['transOrigPointerField'] ?? null;
+        $l10nSourceFieldName = $this->data['processedTca']['ctrl']['translationSource'] ?? null;
+
+        $sourceLanguageTitle = '';
+        $fieldValueInParentRow = '';
+        $fieldValueInSourceRow = '';
+        if ($l10nParentFieldName && $this->data['databaseRow'][$l10nParentFieldName] > 0) {
+            if ($l10nSourceFieldName && $this->data['databaseRow'][$l10nSourceFieldName] > 0) {
+                $languageField = $this->data['processedTca']['ctrl']['languageField'] ?? null;
+                if ($languageField
+                    && isset($this->data['sourceLanguageRow'][$languageField])
+                    && $this->data['sourceLanguageRow'][$languageField] > 0
+                ) {
+                    $languageUidOfSourceRow = $this->data['sourceLanguageRow'][$languageField];
+                    $sourceLanguageTitle = $this->data['systemLanguageRows'][$languageUidOfSourceRow]['title'] ?? '';
+                    $fieldValueInSourceRow = $this->data['sourceLanguageRow'][$fieldName] ?? null;
+                }
+            }
+            $fieldValueInParentRow = (string)$this->data['defaultLanguageRow'][$fieldName];
+        }
+
+        $localizationState = State::fromJSON(
+            $this->data['tableName'],
+            $this->data['databaseRow'][$l10nStateFieldName] ?? null
+        );
+
+        $fieldElementName = 'data[' . htmlspecialchars($this->data['tableName']) . ']'
+            . '[' . (int)($this->data['databaseRow']['uid']) . ']'
+            . '[' . htmlspecialchars($l10nStateFieldName) . ']'
+            . '[' . htmlspecialchars($this->data['fieldName']) . ']';
+
+        $html = [];
+        $html[] = '<div class="t3js-l10n-state-container">';
+        $html[] =   '<div>';
+        $html[] =       '<strong>';
+        $html[] =           $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:localizationStateSelector.header');
+        $html[] =       '</strong>';
+        $html[] =   '</div>';
+        $html[] =   '<div class="radio radio-inline">';
+        $html[] =       '<label>';
+        $html[] =           '<input';
+        $html[] =               ' type="radio"';
+        $html[] =               ' name="' . htmlspecialchars($fieldElementName) . '"';
+        $html[] =               ' class="t3js-l10n-state-custom"';
+        $html[] =               ' value="custom"';
+        $html[] =               $localizationState->isCustomState($fieldName) ? ' checked="checked"' : '';
+        $html[] =               ' data-original-language-value=""';
+        $html[] =           '>';
+        $html[] =           $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:localizationStateSelector.customValue');
+        $html[] =       '</label>';
+        $html[] =   '</div>';
+        $html[] =   '<div class="radio radio-inline">';
+        $html[] =       '<label>';
+        $html[] =           '<input';
+        $html[] =               ' type="radio"';
+        $html[] =               ' name="' . htmlspecialchars($fieldElementName) . '"';
+        $html[] =               ' value="parent"';
+        $html[] =               $localizationState->isParentState($fieldName) ? ' checked="checked"' : '';
+        $html[] =               ' data-original-language-value="' . htmlspecialchars((string)$fieldValueInParentRow) . '"';
+        $html[] =           '>';
+        $html[] =           $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:localizationStateSelector.defaultLanguageValue');
+        $html[] =       '</label>';
+        $html[] =   '</div>';
+        if ($fieldValueInSourceRow) {
+            $html[] = '<div class="radio radio-inline">';
+            $html[] =   '<label>';
+            $html[] =       '<input';
+            $html[] =           ' type="radio"';
+            $html[] =           ' name="' . htmlspecialchars($fieldElementName) . '"';
+            $html[] =           ' value="source"';
+            $html[] =           $localizationState->isSourceState($fieldName) ? ' checked="checked"' : '';
+            $html[] =           ' data-original-language-value="' . htmlspecialchars((string)$fieldValueInSourceRow) . '"';
+            $html[] =       '>';
+            $html[] =       sprintf($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:localizationStateSelector.sourceLanguageValue'), htmlspecialchars($sourceLanguageTitle));
+            $html[] =   '</label>';
+            $html[] = '</div>';
+        }
+        $html[] = '</div>';
+
+        $result['html'] = implode(LF, $html);
+        return $result;
+    }
+
+    /**
+     * @return LanguageService
+     */
+    protected function getLanguageService()
+    {
+        return $GLOBALS['LANG'];
+    }
+}
index 4bc0ad0..4e572fc 100644 (file)
@@ -187,6 +187,9 @@ class FormDataCompiler
             'pageLanguageOverlayRows' => [],
             // If the handled row is a localized row, this entry hold the default language row array
             'defaultLanguageRow' => null,
+            // If the handled row is a localived row and $TCA[<tableName>]['ctrl']['translationSource'] is configured,
+            // This entry holds the row of the language source record.
+            'sourceLanguageRow' => null,
             // If the handled row is a localized row and a transOrigDiffSourceField is defined, this
             // is the unserialized version of it. The diff source field is basically a shadow version
             // of the default language record at the time when the language overlay record was created.
index 74b4be8..34f0ea4 100644 (file)
@@ -103,6 +103,22 @@ class DatabaseLanguageRows implements FormDataProviderInterface
                         }
                     }
                 }
+
+                // @todo do that only if l10n_parent > 0 (not in "free mode")?
+                if (!empty($result['processedTca']['ctrl']['translationSource'])
+                    && is_string($result['processedTca']['ctrl']['translationSource'])
+                ) {
+                    $translationSourceFieldName = $result['processedTca']['ctrl']['translationSource'];
+                    if (isset($result['databaseRow'][$translationSourceFieldName])
+                        && $result['databaseRow'][$translationSourceFieldName] > 0
+                    ) {
+                        $uidOfTranslationSource = $result['databaseRow'][$translationSourceFieldName];
+                        $result['sourceLanguageRow'] = $this->getRecordWorkspaceOverlay(
+                            $result['tableName'],
+                            $uidOfTranslationSource
+                        );
+                    }
+                }
             }
         }
 
index 90fdb5b..2e2f4c4 100644 (file)
@@ -64,7 +64,7 @@ class DatabaseRecordTypeValue implements FormDataProviderInterface
                         1438183881
                     );
                 }
-                $recordTypeValue = $this->getValueFromDefaultLanguageRecordIfConfigured($result, $tcaTypeField);
+                $recordTypeValue = $result['databaseRow'][$tcaTypeField];
             } else {
                 // If type is configured as localField:foreignField, fetch the type value from
                 // a foreign table. localField then point to a group or select field in the own table,
@@ -81,7 +81,7 @@ class DatabaseRecordTypeValue implements FormDataProviderInterface
                     );
                 }
 
-                $foreignUid = $this->getValueFromDefaultLanguageRecordIfConfigured($result, $pointerField);
+                $foreignUid = $result['databaseRow'][$pointerField];
                 // Resolve the foreign record only if there is a uid, otherwise fall back 0
                 if (!empty($foreignUid)) {
                     // Determine table name to fetch record from
@@ -149,29 +149,4 @@ class DatabaseRecordTypeValue implements FormDataProviderInterface
 
         return $row ?: [];
     }
-
-    /**
-     * If a localized row is handled, the field value of the default language record
-     * is used instead if tca is configured as "exclude" with empty localized value.
-     *
-     * @param array $result Main "$result" data array
-     * @param string $field Field name to fetch value for
-     * @return string field value
-     */
-    protected function getValueFromDefaultLanguageRecordIfConfigured($result, $field)
-    {
-        $value = $result['databaseRow'][$field];
-        if (
-            // is a localized record
-            !empty($result['processedTca']['ctrl']['languageField'])
-            && $result['databaseRow'][$result['processedTca']['ctrl']['languageField']] > 0
-            // l10n_mode for field is configured
-            && !empty($result['processedTca']['columns'][$field]['l10n_mode'])
-            // is exclude -> fall back to value of default record
-            && $result['processedTca']['columns'][$field]['l10n_mode'] === 'exclude'
-        ) {
-            $value = $result['defaultLanguageRow'][$field];
-        }
-        return $value;
-    }
 }
index 422fcd5..ef6b266 100644 (file)
@@ -101,6 +101,7 @@ class NodeFactory
         'fileThumbnails' => FieldWizard\FileThumbnails::class,
         'fileTypeList' => FieldWizard\FileTypeList::class,
         'fileUpload' => FieldWizard\FileUpload::class,
+        'localizationStateSelector' => FieldWizard\LocalizationStateSelector::class,
         'otherLanguageContent' => FieldWizard\OtherLanguageContent::class,
         'recordsOverview' => FieldWizard\RecordsOverview::class,
         'selectIcons' => FieldWizard\SelectIcons::class,
index 51b1f92..ccc6952 100644 (file)
@@ -701,6 +701,34 @@ define(['jquery',
                }).on('change', '.t3js-form-field-eval-null-placeholder-checkbox input[type="checkbox"]', function(e) {
                        $(this).closest('.t3js-formengine-field-item').find('.t3js-formengine-placeholder-placeholder').toggle();
                        $(this).closest('.t3js-formengine-field-item').find('.t3js-formengine-placeholder-formfield').toggle();
+               }).on('change', '.t3js-l10n-state-container input[type=radio]', function(event) {
+                       // Change handler for "l10n_state" field changes
+                       var $me = $(this);
+                       var $input = $me.closest('.t3js-formengine-field-item').find('[data-formengine-input-name]');
+
+                       if ($input.length > 0) {
+                               var lastState = $input.data('last-l10n-state') || false,
+                                       currentState = $(this).val();
+
+                               if (lastState && currentState === lastState) {
+                                       return;
+                               }
+
+                               if (currentState === 'custom') {
+                                       if (lastState) {
+                                               $(this).attr('data-original-language-value', $input.val());
+                                       }
+                                       $input.attr('disabled', false);
+                               } else {
+                                       if (lastState === 'custom') {
+                                               $(this).closest('.t3js-l10n-state-container').find('.t3js-l10n-state-custom').attr('data-original-language-value', $input.val());
+                                       }
+                                       $input.attr('disabled', 'disabled');
+                               }
+
+                               $input.val($(this).attr('data-original-language-value')).trigger('change');
+                               $input.data('last-l10n-state', $(this).val());
+                       }
                });
        };
 
@@ -956,12 +984,26 @@ define(['jquery',
                FormEngine.initializeNullNoPlaceholderCheckboxes();
                FormEngine.initializeNullWithPlaceholderCheckboxes();
                FormEngine.initializeInputLinkToggle();
+               FormEngine.initializeLocalizationStateSelector();
+       };
+
+       /**
+        * Disable the input field on load if localization state selector is set to "parent" or "source"
+        */
+       FormEngine.initializeLocalizationStateSelector = function() {
+               $('.t3js-l10n-state-container').each(function() {
+                       var $input = $(this).closest('.t3js-formengine-field-item').find('[data-formengine-input-name]');
+                       var currentState = $(this).find('input[type="radio"]:checked').val();
+                       if (currentState === 'parent' || currentState === 'source') {
+                               $input.attr('disabled', 'disabled');
+                       }
+               });
        };
 
        /**
         * Toggle for input link explanation
         */
-       FormEngine.initializeInputLinkToggle = function () {
+       FormEngine.initializeInputLinkToggle = function() {
                $(document).on('click', '.t3js-form-field-inputlink-explanation-toggle', function(e) {
                        e.preventDefault();
 
index df93e6a..2bc0d4d 100644 (file)
@@ -367,4 +367,77 @@ class DatabaseLanguageRowsTest extends \TYPO3\Components\TestingFramework\Core\U
 
         $this->assertEquals($expected, $this->subject->addData($input));
     }
+
+    /**
+     * @test
+     */
+    public function addDataSetsSourceLanguageRow()
+    {
+        $input = [
+            'tableName' => 'tt_content',
+            'databaseRow' => [
+                'uid' => 42,
+                'text' => 'localized text',
+                'sys_language_uid' => 3,
+                'l10n_parent' => 23,
+                'l10n_source' => 24,
+            ],
+            'processedTca' => [
+                'ctrl' => [
+                    'languageField' => 'sys_language_uid',
+                    'transOrigPointerField' => 'l10n_parent',
+                    'translationSource' => 'l10n_source',
+                ],
+            ],
+            'systemLanguageRows' => [
+                0 => [
+                    'uid' => 0,
+                    'title' => 'Default Language',
+                    'iso' => 'DEV',
+                ],
+                2 => [
+                    'uid' => 2,
+                    'title' => 'dansk',
+                    'iso' => 'dk,'
+                ],
+                3 => [
+                    'uid' => 3,
+                    'title' => 'french',
+                    'iso' => 'fr',
+                ],
+            ],
+            'defaultLanguageRow' => null,
+            'sourceLanguageRow' => null,
+            'additionalLanguageRows' => [],
+        ];
+
+        // For BackendUtility::getRecord()
+        $GLOBALS['TCA']['tt_content'] = ['foo'];
+        $sourceLanguageRow = [
+            'uid' => 24,
+            'pid' => 32,
+            'text' => 'localized text in dank',
+            'sys_language_uid' => 2,
+        ];
+        $defaultLanguageRow = [
+            'uid' => 23,
+            'pid' => 32,
+            'text' => 'default language text',
+            'sys_language_uid' => 0,
+        ];
+        $this->subject->expects($this->at(0))
+            ->method('getRecordWorkspaceOverlay')
+            ->with('tt_content', 23)
+            ->willReturn($defaultLanguageRow);
+        $this->subject->expects($this->at(1))
+            ->method('getRecordWorkspaceOverlay')
+            ->with('tt_content', 24)
+            ->willReturn($sourceLanguageRow);
+
+        $expected = $input;
+        $expected['defaultLanguageRow'] = $defaultLanguageRow;
+        $expected['sourceLanguageRow'] = $sourceLanguageRow;
+
+        $this->assertEquals($expected, $this->subject->addData($input));
+    }
 }
index 72b53a6..772a72d 100644 (file)
@@ -262,42 +262,6 @@ class DatabaseRecordTypeValueTest extends \TYPO3\Components\TestingFramework\Cor
     /**
      * @test
      */
-    public function addDataSetsRecordTypeValueToValueOfDefaultLanguageRecordIfConfiguredAsExclude()
-    {
-        $input = [
-            'recordTypeValue' => '',
-            'processedTca' => [
-                'ctrl' => [
-                    'languageField' => 'sys_language_uid',
-                    'type' => 'aField',
-                ],
-                'columns' => [
-                    'aField' => [
-                        'l10n_mode' => 'exclude',
-                    ],
-                ],
-                'types' => [
-                    '3' => 'foo',
-                ],
-            ],
-            'databaseRow' => [
-                'sys_language_uid' => 2,
-                'aField' => 4,
-            ],
-            'defaultLanguageRow' => [
-                'aField' => 3,
-            ],
-        ];
-
-        $expected = $input;
-        $expected['recordTypeValue'] = '3';
-
-        $this->assertSame($expected, $this->subject->addData($input));
-    }
-
-    /**
-     * @test
-     */
     public function addDataThrowsExceptionForForeignTypeConfigurationNotAsSelectOrGroup()
     {
         $input = [
index 13eefbf..708d3bf 100644 (file)
@@ -31,6 +31,7 @@ use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
 use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface;
 use TYPO3\CMS\Core\Database\ReferenceIndex;
 use TYPO3\CMS\Core\Database\RelationHandler;
+use TYPO3\CMS\Core\DataHandling\Localization\DataMapProcessor;
 use TYPO3\CMS\Core\Html\RteHtmlParser;
 use TYPO3\CMS\Core\Messaging\FlashMessage;
 use TYPO3\CMS\Core\Messaging\FlashMessageService;
@@ -977,6 +978,8 @@ class DataHandler
                 $hookObjectsArr[] = $hookObject;
             }
         }
+        // Pre-process data-map and synchronize localization states
+        $this->datamap = DataMapProcessor::instance($this->datamap, $this->BE_USER)->process();
         // Organize tables so that the pages-table is always processed first. This is required if you want to make sure that content pointing to a new page will be created.
         $orderOfTables = [];
         // Set pages first.
@@ -1500,6 +1503,9 @@ class DataHandler
                 case 't3ver_tstamp':
                     // t3ver_label is not here because it CAN be edited as a regular field!
                     break;
+                case 'l10n_state':
+                    $fieldArray[$field] = $fieldValue;
+                    break;
                 default:
                     if (isset($GLOBALS['TCA'][$table]['columns'][$field])) {
                         // Evaluating the value
diff --git a/typo3/sysext/core/Classes/DataHandling/DatabaseSchemaService.php b/typo3/sysext/core/Classes/DataHandling/DatabaseSchemaService.php
new file mode 100644 (file)
index 0000000..f4886d3
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+namespace TYPO3\CMS\Core\DataHandling;
+
+/*
+ * 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!
+ */
+
+/**
+ * This service provides the sql schema database records.
+ */
+class DatabaseSchemaService
+{
+    const TABLE_TEMPLATE = 'CREATE TABLE %s (' . LF . '%s' . LF . ');';
+    const FIELD_L10N_STATE_TEMPLATE = '  l10n_state text';
+
+    /**
+     * Add l10n_state field to tables that provide localization
+     *
+     * @return string Localization fields database schema
+     */
+    public function getLocalizationRequiredDatabaseSchema(array $sqlString)
+    {
+        $tableSchemas = [];
+
+        foreach ($GLOBALS['TCA'] as $tableName => $tableDefinition) {
+            if (
+                empty($tableDefinition['columns'])
+                || empty($tableDefinition['ctrl']['languageField'])
+                || empty($tableDefinition['ctrl']['transOrigPointerField'])
+            ) {
+                continue;
+            }
+
+            $fieldSchemas = [];
+            $fieldSchemas[] = static::FIELD_L10N_STATE_TEMPLATE;
+
+            $tableSchemas[] = sprintf(
+                static::TABLE_TEMPLATE,
+                $tableName,
+                implode(',' . LF, $fieldSchemas)
+            );
+        }
+
+        if (!empty($tableSchemas)) {
+            $sqlString[] = implode(LF, $tableSchemas);
+        }
+
+        return array('sqlString' => $sqlString);
+    }
+}
diff --git a/typo3/sysext/core/Classes/DataHandling/Localization/DataMapItem.php b/typo3/sysext/core/Classes/DataHandling/Localization/DataMapItem.php
new file mode 100644 (file)
index 0000000..6acc9a6
--- /dev/null
@@ -0,0 +1,430 @@
+<?php
+namespace TYPO3\CMS\Core\DataHandling\Localization;
+
+/*
+ * 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 TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MathUtility;
+
+/**
+ * Entity for data-map item.
+ */
+class DataMapItem
+{
+    const TYPE_PARENT = 'parent';
+    const TYPE_DIRECT_CHILD = 'directChild';
+    const TYPE_GRAND_CHILD = 'grandChild';
+
+    const SCOPE_PARENT = State::STATE_PARENT;
+    const SCOPE_SOURCE = State::STATE_SOURCE;
+    const SCOPE_EXCLUDE = 'exclude';
+
+    /**
+     * @var string
+     */
+    protected $tableName;
+
+    /**
+     * @var string|int
+     */
+    protected $id;
+
+    /**
+     * @var array
+     */
+    protected $suggestedValues;
+
+    /**
+     * @var array
+     */
+    protected $persistedValues;
+
+    /**
+     * @var array
+     */
+    protected $configurationFieldNames;
+
+    /**
+     * @var bool
+     */
+    protected $new;
+
+    /**
+     * @var string
+     */
+    protected $type;
+
+    /**
+     * @var State
+     */
+    protected $state;
+
+    /**
+     * @var string|int
+     */
+    protected $language;
+
+    /**
+     * @var string|int
+     */
+    protected $parent;
+
+    /**
+     * @var string|int
+     */
+    protected $source;
+
+    /**
+     * @var DataMapItem[][]
+     */
+    protected $dependencies = [];
+
+    /**
+     * Builds a data-map item. In addition to the constructor, the values
+     * for language, parent and source record pointers are assigned as well.
+     *
+     * @param string $tableName
+     * @param string|int $id
+     * @param array $suggestedValues
+     * @param array $persistedValues
+     * @param array $configurationFieldNames
+     * @return object|DataMapItem
+     */
+    public static function build(
+        string $tableName,
+        $id,
+        array $suggestedValues,
+        array $persistedValues,
+        array $configurationFieldNames
+    ) {
+        $item = GeneralUtility::makeInstance(
+            static::class,
+            $tableName,
+            $id,
+            $suggestedValues,
+            $persistedValues,
+            $configurationFieldNames
+        );
+
+        $item->language = (int)($suggestedValues[$item->getLanguageFieldName()] ?? $persistedValues[$item->getLanguageFieldName()]);
+        $item->setParent($suggestedValues[$item->getParentFieldName()] ?? $persistedValues[$item->getParentFieldName()]);
+        if ($item->getSourceFieldName() !== null) {
+            $item->setSource($suggestedValues[$item->getSourceFieldName()] ?? $persistedValues[$item->getSourceFieldName()]);
+        }
+
+        return $item;
+    }
+
+    /**
+     * @param string $tableName
+     * @param string|int $id
+     * @param array $suggestedValues
+     * @param array $persistedValues
+     * @param array $configurationFieldNames
+     */
+    public function __construct(
+        string $tableName,
+        $id,
+        array $suggestedValues,
+        array $persistedValues,
+        array $configurationFieldNames
+    ) {
+        $this->tableName = $tableName;
+        $this->id = $id;
+
+        $this->suggestedValues = $suggestedValues;
+        $this->persistedValues = $persistedValues;
+        $this->configurationFieldNames = $configurationFieldNames;
+
+        $this->new = !MathUtility::canBeInterpretedAsInteger($id);
+    }
+
+    /**
+     * Gets the current table name of this data-map item.
+     *
+     * @return string
+     */
+    public function getTableName(): string
+    {
+        return $this->tableName;
+    }
+
+    /**
+     * Gets the table name used to resolve the language parent record.
+     *
+     * @return string
+     */
+    public function getFromTableName(): string
+    {
+        if ($this->tableName === 'pages_language_overlay') {
+            return 'pages';
+        }
+        return $this->tableName;
+    }
+
+    /**
+     * Gets the table name used to resolve any kind of translations.
+     *
+     * @return string
+     */
+    public function getForTableName(): string
+    {
+        if ($this->tableName === 'pages') {
+            return 'pages_language_overlay';
+        }
+        return $this->tableName;
+    }
+
+    /**
+     * Gets the id of this data-map item.
+     *
+     * @return mixed
+     */
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    /**
+     * Gets the suggested values that were initially
+     * submitted as the whole data-map to the DataHandler.
+     *
+     * @return array
+     */
+    public function getSuggestedValues(): array
+    {
+        return $this->suggestedValues;
+    }
+
+    /**
+     * Gets the persisted values that represent the persisted state
+     * of the record this data-map item is a surrogate for - does only
+     * contain relevant field values.
+     *
+     * @return array
+     */
+    public function getPersistedValues(): array
+    {
+        return $this->persistedValues;
+    }
+
+    /**
+     * @return array
+     */
+    public function getConfigurationFieldNames(): array
+    {
+        return $this->configurationFieldNames;
+    }
+
+    /**
+     * @return string
+     */
+    public function getLanguageFieldName(): string
+    {
+        return $this->configurationFieldNames['language'];
+    }
+
+    /**
+     * @return string
+     */
+    public function getParentFieldName(): string
+    {
+        return $this->configurationFieldNames['parent'];
+    }
+
+    /**
+     * @return null|string
+     */
+    public function getSourceFieldName()
+    {
+        return $this->configurationFieldNames['source'];
+    }
+
+    /**
+     * @return bool
+     */
+    public function isNew(): bool
+    {
+        return $this->new;
+    }
+
+    /**
+     * @return string
+     */
+    public function getType(): string
+    {
+        if ($this->type === null) {
+            // implicit: default language, it's a parent
+            if ($this->language === 0) {
+                $this->type = static::TYPE_PARENT;
+            // implicit: having source value different to parent value, it's a 2nd or higher level translation
+            } elseif (
+                $this->source !== null
+                && $this->source !== $this->parent
+            ) {
+                $this->type = static::TYPE_GRAND_CHILD;
+            // implicit: otherwise, it's a 1st level translation
+            } else {
+                $this->type = static::TYPE_DIRECT_CHILD;
+            }
+        }
+        return $this->type;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isParentType(): bool
+    {
+        return $this->getType() === static::TYPE_PARENT;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isDirectChildType(): bool
+    {
+        return $this->getType() === static::TYPE_DIRECT_CHILD;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isGrandChildType(): bool
+    {
+        return $this->getType() === static::TYPE_GRAND_CHILD;
+    }
+
+    /**
+     * @return State
+     */
+    public function getState(): State
+    {
+        if ($this->state === null && !$this->isParentType()) {
+            $this->state = State::fromJSON(
+                $this->tableName,
+                $this->persistedValues['l10n_state'] ?? null
+            );
+            $this->state->update(
+                $this->suggestedValues['l10n_state'] ?? []
+            );
+        }
+        return $this->state;
+    }
+
+    /**
+     * @return string|int
+     */
+    public function getLanguage()
+    {
+        return $this->language;
+    }
+
+    /**
+     * @param string|int $language
+     */
+    public function setLanguage($language)
+    {
+        $this->language = $language;
+    }
+
+    /**
+     * @return string|int
+     */
+    public function getParent()
+    {
+        return $this->parent;
+    }
+
+    /**
+     * @param string|int $parent
+     */
+    public function setParent($parent)
+    {
+        $this->parent = $parent;
+    }
+
+    /**
+     * @return string|int
+     */
+    public function getSource()
+    {
+        return $this->source;
+    }
+
+    /**
+     * @param string|int $source
+     */
+    public function setSource($source)
+    {
+        $this->source = $source;
+    }
+
+    /**
+     * @param string $scope
+     * @return int|string
+     */
+    public function getIdForScope($scope)
+    {
+        if (
+            $scope === static::SCOPE_PARENT
+            || $scope === static::SCOPE_EXCLUDE
+        ) {
+            return $this->getParent();
+        }
+        if ($scope === static::SCOPE_SOURCE) {
+            return $this->getSource();
+        }
+        throw new \RuntimeException('Invalid scope', 1486325248);
+    }
+
+    /**
+     * @return DataMapItem[][]
+     */
+    public function getDependencies(): array
+    {
+        return $this->dependencies;
+    }
+
+    /**
+     * @param DataMapItem[][] $dependencies
+     */
+    public function setDependencies(array $dependencies)
+    {
+        $this->dependencies = $dependencies;
+    }
+
+    /**
+     * @param string $scope
+     * @return DataMapItem[]
+     */
+    public function findDependencies(string $scope)
+    {
+        return ($this->dependencies[$scope] ?? []);
+    }
+
+    /**
+     * @return string[]
+     */
+    public function getApplicableScopes()
+    {
+        $scopes = [];
+        if (!empty($this->getSourceFieldName())) {
+            $scopes[] = static::SCOPE_SOURCE;
+        }
+        $scopes[] = static::SCOPE_PARENT;
+        $scopes[] = static::SCOPE_EXCLUDE;
+        return $scopes;
+    }
+}
\ No newline at end of file
diff --git a/typo3/sysext/core/Classes/DataHandling/Localization/DataMapProcessor.php b/typo3/sysext/core/Classes/DataHandling/Localization/DataMapProcessor.php
new file mode 100644 (file)
index 0000000..d86f73b
--- /dev/null
@@ -0,0 +1,894 @@
+<?php
+namespace TYPO3\CMS\Core\DataHandling\Localization;
+
+/*
+ * 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 TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
+use TYPO3\CMS\Core\Database\RelationHandler;
+use TYPO3\CMS\Core\DataHandling\DataHandler;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MathUtility;
+use TYPO3\CMS\Core\Utility\StringUtility;
+
+/**
+ * This processor analyses the provided data-map before actually being process
+ * in the calling DataHandler instance. Field names that are configured to have
+ * "allowLanguageSynchronization" enabled are either synchronized from there
+ * relative parent records (could be a default language record, or a l10n_source
+ * record) or to their dependent records (in case a default language record or
+ * nested records pointing upwards with l10n_source).
+ *
+ * Except inline relational record editing, all modifications are applied to
+ * the data-map directly, which ensures proper history entries as a side-effect.
+ * For inline relational record editing, this processor either triggers the copy
+ * or localize actions by instantiation a new local DataHandler instance.
+ */
+class DataMapProcessor
+{
+    /**
+     * @var array
+     */
+    protected $dataMap = [];
+
+    /**
+     * @var BackendUserAuthentication
+     */
+    protected $backendUser;
+
+    /**
+     * @var DataMapItem[]
+     */
+    protected $items = [];
+
+    /**
+     * Class generator
+     *
+     * @param array $dataMap The submitted data-map to be worked on
+     * @param BackendUserAuthentication $backendUser Forwared backend-user scope
+     * @return DataMapProcessor
+     */
+    public static function instance(array $dataMap, BackendUserAuthentication $backendUser)
+    {
+        return GeneralUtility::makeInstance(
+            static::class,
+            $dataMap,
+            $backendUser
+        );
+    }
+
+    /**
+     * @param array $dataMap The submitted data-map to be worked on
+     * @param BackendUserAuthentication $backendUser Forwared backend-user scope
+     */
+    public function __construct(array $dataMap, BackendUserAuthentication $backendUser)
+    {
+        $this->dataMap = $dataMap;
+        $this->backendUser = $backendUser;
+    }
+
+    /**
+     * Processes the submitted data-map and returns the sanitized and enriched
+     * version depending on accordant localization states and dependencies.
+     *
+     * @return array
+     */
+    public function process()
+    {
+        foreach ($this->dataMap as $tableName => $idValues) {
+            $this->collectItems($tableName, $idValues);
+        }
+        $this->sanitize();
+        $this->enrich();
+        return $this->dataMap;
+    }
+
+    /**
+     * Create data map items of all affected rows
+     *
+     * @param string $tableName
+     * @param array $idValues
+     */
+    protected function collectItems(string $tableName, array $idValues)
+    {
+        if (!$this->isApplicable($tableName)) {
+            return;
+        }
+
+        $fieldNames = [
+            'uid' => 'uid',
+            'l10n_state' => 'l10n_state',
+            'language' => $GLOBALS['TCA'][$tableName]['ctrl']['languageField'],
+            'parent' => $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'],
+        ];
+        if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['translationSource'])) {
+            $fieldNames['source'] = $GLOBALS['TCA'][$tableName]['ctrl']['translationSource'];
+        }
+
+        $translationValues = $this->fetchTranslationValues(
+            $tableName,
+            $fieldNames,
+            $this->filterNumericIds(array_keys($idValues))
+        );
+
+        $dependencies = $this->fetchDependencies(
+            $tableName,
+            $this->filterNumericIds(array_keys($idValues))
+        );
+
+        foreach ($idValues as $id => $values)
+        {
+            $recordValues = $translationValues[$id] ?? [];
+//            $values['l10n_state'] = json_decode($values['l10n_state'], true) ?? [];
+            $item = DataMapItem::build(
+                $tableName,
+                $id,
+                $values,
+                $recordValues,
+                $fieldNames
+            );
+
+            // must be any kind of localization and in connected mode
+            if ($item->getLanguage() > 0 && empty($item->getParent())) {
+                unset($item);
+                continue;
+            }
+            // add dependencies
+            if (!empty($dependencies[$id])) {
+                $item->setDependencies($dependencies[$id]);
+            }
+            $this->items[$tableName . ':' . $id] = $item;
+        }
+    }
+
+    /**
+     * Sanitizes the submitted data-map and removes fields which are not
+     * defined as custom and thus rely on either parent or source values.
+     */
+    protected function sanitize()
+    {
+        foreach (['grandChild', 'directChild'] as $type) {
+            foreach ($this->filterItemsByType($type) as $item) {
+                $this->sanitizeTranslationItem($item);
+            }
+        }
+    }
+
+    /**
+     * Handle synchronization of an item list
+     */
+    protected function enrich()
+    {
+        foreach (['grandChild', 'directChild'] as $type) {
+            foreach ($this->filterItemsByType($type) as $item) {
+                foreach ($item->getApplicableScopes() as $scope) {
+                    $fromId = $item->getIdForScope($scope);
+                    $fieldNames = $this->getFieldNamesForItemScope($item, $scope, !$item->isNew());
+                    $this->synchronizeTranslationItem($item, $fieldNames, $fromId);
+                }
+                $this->populateTranslationItem($item);
+                $this->finishTranslationItem($item);
+            }
+        }
+        foreach ($this->filterItemsByType('parent') as $item) {
+            $this->populateTranslationItem($item);
+        }
+    }
+
+    /**
+     * Sanitizes the submitted data-map for a particular item and removes
+     * fields which are not defined as custom and thus rely on either parent
+     * or source values.
+     *
+     * @param DataMapItem $item
+     */
+    protected function sanitizeTranslationItem(DataMapItem $item)
+    {
+        $fieldNames = array_merge(
+            $this->getFieldNamesForItemScope($item, DataMapItem::SCOPE_PARENT, !$item->isNew()),
+            $this->getFieldNamesForItemScope($item, DataMapItem::SCOPE_SOURCE, !$item->isNew())
+        );
+        // remove fields, that are submitted in data-map, but not defined as custom
+        $this->dataMap[$item->getTableName()][$item->getId()] = array_diff_key(
+            $this->dataMap[$item->getTableName()][$item->getId()],
+            array_combine($fieldNames, $fieldNames)
+        );
+    }
+
+    /**
+     * Synchronize a single item
+     *
+     * @param DataMapItem $item
+     * @param array $fieldNames
+     * @param int $fromId
+     */
+    protected function synchronizeTranslationItem(DataMapItem $item, array $fieldNames, int $fromId)
+    {
+        if (empty($fieldNames)) {
+            return;
+        }
+        $fieldNameList = 'uid,' . implode(',', $fieldNames);
+        $fromRecord = BackendUtility::getRecordWSOL(
+            $item->getFromTableName(),
+            $fromId,
+            $fieldNameList
+        );
+        $forRecord = [];
+        if (!$item->isNew()) {
+            $forRecord = BackendUtility::getRecordWSOL(
+                $item->getTableName(),
+                $item->getId(),
+                $fieldNameList
+            );
+        }
+        foreach ($fieldNames as $fieldName) {
+            $this->synchronizeFieldValues(
+                $item,
+                $fieldName,
+                $fromRecord,
+                $forRecord
+            );
+        }
+    }
+
+    /**
+     * Populates values downwards, either from a parent language item or
+     * a source language item to an accordant dependent translation item.
+     *
+     * @param DataMapItem $item
+     */
+    protected function populateTranslationItem(DataMapItem $item)
+    {
+        if ($item->isNew()) {
+            return;
+        }
+
+        foreach ([State::STATE_PARENT, State::STATE_SOURCE] as $scope) {
+            foreach ($item->findDependencies($scope) as $dependentItem) {
+                // use suggested item, if it was submitted in data-map
+                $suggestedDependentItem = $this->findItem(
+                    $dependentItem->getTableName(),
+                    $dependentItem->getId()
+                );
+                if ($suggestedDependentItem !== null) {
+                    $dependentItem = $suggestedDependentItem;
+                }
+                $fieldNames = $this->getFieldNamesForItemScope(
+                    $dependentItem,
+                    $scope,
+                    false
+                );
+                $this->synchronizeTranslationItem(
+                    $dependentItem,
+                    $fieldNames,
+                    $item->getId()
+                );
+            }
+        }
+    }
+
+    /**
+     * Finishes a translation item by updating states to be persisted.
+     *
+     * @param DataMapItem $item
+     */
+    protected function finishTranslationItem(DataMapItem $item)
+    {
+        if (
+            $item->isParentType()
+            || !State::isApplicable($item->getTableName())
+        ) {
+            return;
+        }
+
+        $this->dataMap[$item->getTableName()][$item->getId()]['l10n_state'] = $item->getState()->export();
+    }
+
+    /**
+     * Synchronize simple values like text and similar
+     *
+     * @param DataMapItem $item
+     * @param string $fieldName
+     * @param array $fromRecord
+     * @param array $forRecord
+     */
+    protected function synchronizeFieldValues(DataMapItem $item, string $fieldName, array $fromRecord, array $forRecord)
+    {
+        // skip if this field has been processed already, assumed that proper sanitation happened
+        if (!empty($this->dataMap[$item->getTableName()][$item->getId()][$fieldName])) {
+            return;
+        }
+
+        $fromId = $fromRecord['uid'];
+        $fromValue = $this->dataMap[$item->getFromTableName()][$fromId][$fieldName] ?? $fromRecord[$fieldName];
+
+        // plain values
+        if (!$this->isRelationField($item->getFromTableName(), $fieldName)) {
+            $this->dataMap[$item->getTableName()][$item->getId()][$fieldName] = $fromValue;
+        // direct relational values
+        } elseif (!$this->isInlineRelationField($item->getFromTableName(), $fieldName)) {
+            $this->synchronizeDirectRelations($item, $fieldName, $fromRecord);
+        // inline relational values
+        } else {
+            $this->synchronizeInlineRelations($item, $fieldName, $fromRecord, $forRecord);
+        }
+    }
+
+    /**
+     * Synchronize select and group field localizations
+     *
+     * @param DataMapItem $item
+     * @param string $fieldName
+     * @param array $fromRecord
+     */
+    protected function synchronizeDirectRelations(DataMapItem $item, string $fieldName, array $fromRecord)
+    {
+        $fromId = $fromRecord['uid'];
+        $fromValue = $this->dataMap[$item->getFromTableName()][$fromId][$fieldName] ?? $fromRecord[$fieldName];
+        $configuration = $GLOBALS['TCA'][$item->getFromTableName()]['columns'][$fieldName];
+
+        // non-MM relations are stored as comma separated values, just use them
+        // if values are available in data-map already, just use them as well
+        if (
+            empty($configuration['config']['MM'])
+            || isset($this->dataMap[$item->getFromTableName()][$fromId][$fieldName])
+            || ($configuration['config']['special'] ?? null) === 'languages'
+        ) {
+            $this->dataMap[$item->getTableName()][$item->getId()][$fieldName] = $fromValue;
+            return;
+        }
+
+        // fetch MM relations from storage
+        $type = $configuration['config']['type'];
+        $manyToManyTable = $configuration['config']['MM'];
+        if ($type === 'group' && $configuration['config']['internal_type'] === 'db') {
+            $tableNames = trim($configuration['config']['allowed'] ?? '');
+        } elseif ($configuration['config']['type'] === 'select') {
+            $tableNames = ($configuration['foreign_table'] ?? '');
+        } else {
+            return;
+        }
+
+        $relationHandler = $this->createRelationHandler();
+        $relationHandler->start(
+            '',
+            $tableNames,
+            $manyToManyTable,
+            $fromId,
+            $item->getFromTableName(),
+            $configuration['config']
+        );
+
+        // provide list of relations, optionally prepended with table name
+        // e.g. "13,19,23" or "tt_content_27,tx_extension_items_28"
+        $this->dataMap[$item->getTableName()][$item->getId()][$fieldName] = implode(
+            ',',
+            $relationHandler->getValueArray()
+        );
+    }
+
+    /**
+     * Handle synchonization of inline relations
+     *
+     * @param DataMapItem $item
+     * @param string $fieldName
+     * @param array $fromRecord
+     * @param array $forRecord
+     */
+    protected function synchronizeInlineRelations(DataMapItem $item, string $fieldName, array $fromRecord, array $forRecord)
+    {
+        $fromId = $fromRecord['uid'];
+        $configuration = $GLOBALS['TCA'][$item->getFromTableName()]['columns'][$fieldName];
+        $foreignTableName = $configuration['config']['foreign_table'];
+        $manyToManyTable = ($configuration['config']['MM'] ?? '');
+
+        $languageFieldName = ($GLOBALS['TCA'][$foreignTableName]['ctrl']['languageField'] ?? null);
+        $parentFieldName = ($GLOBALS['TCA'][$foreignTableName]['ctrl']['transOrigPointerField'] ?? null);
+        $sourceFieldName = ($GLOBALS['TCA'][$foreignTableName]['ctrl']['translationSource'] ?? null);
+
+        // determine suggested elements of either translation parent or source record
+        // from data-map, in case the accordant language parent/source record was modified
+        if (isset($this->dataMap[$item->getFromTableName()][$fromId][$fieldName])) {
+            $suggestedAncestorIds = GeneralUtility::trimExplode(
+                ',',
+                $this->dataMap[$item->getFromTableName()][$fromId][$fieldName],
+                true
+            );
+        // determine suggested elements of either translation parent or source record from storage
+        } else {
+            $relationHandler = $this->createRelationHandler();
+            $relationHandler->start(
+                $fromRecord[$fieldName],
+                $foreignTableName,
+                $manyToManyTable,
+                $fromId,
+                $item->getFromTableName(),
+                $configuration['config']
+            );
+            $suggestedAncestorIds = $this->mapRelationItemId($relationHandler->itemArray);
+        }
+        // determine persisted elements for the current data-map item
+        $relationHandler = $this->createRelationHandler();
+        $relationHandler->start(
+            $forRecord[$fieldName] ?? '',
+            $foreignTableName,
+            $manyToManyTable,
+            $item->getId(),
+            $item->getTableName(),
+            $configuration['config']
+        );
+        $persistedIds = $this->mapRelationItemId($relationHandler->itemArray);
+        // The dependent ID map points from language parent/source record to
+        // localization, thus keys: parents/sources & values: localizations
+        $dependentIdMap = $this->fetchDependentIdMap($foreignTableName, $suggestedAncestorIds);
+        // filter incomplete structures - this is a drawback of DataHandler's remap stack, since
+        // just created IRRE translations still belong to the language parent - filter them out
+        $suggestedAncestorIds = array_diff($suggestedAncestorIds, array_values($dependentIdMap));
+        // compile element differences to be resolved
+        // remove elements that are persisted at the language translation, but not required anymore
+        $removeIds = array_diff($persistedIds, array_values($dependentIdMap));
+        // remove elements that are persisted at the language parent/source, but not required anymore
+        $removeAncestorIds = array_diff(array_keys($dependentIdMap), $suggestedAncestorIds);
+        // missing elements that are persisted at the language parent/source, but not translated yet
+        $missingAncestorIds = array_diff($suggestedAncestorIds, array_keys($dependentIdMap));
+        // persisted elements that should be copied or localized
+        $createAncestorIds = $this->filterNumericIds($missingAncestorIds, true);
+        // non-persisted elements that should be duplicated in data-map directly
+        $populateAncestorIds = $this->filterNumericIds($missingAncestorIds, false);
+        // this desired state map defines the final result of child elements of the translation
+        $desiredLocalizationIdMap = array_combine($suggestedAncestorIds, $suggestedAncestorIds);
+        // update existing translations in the desired state map
+        foreach ($dependentIdMap as $ancestorId => $translationId) {
+            if (isset($desiredLocalizationIdMap[$ancestorId])) {
+                $desiredLocalizationIdMap[$ancestorId] = $translationId;
+            }
+        }
+        // nothing to synchronize, but element order could have been changed
+        if (empty($removeAncestorIds) && empty($missingAncestorIds)) {
+            $this->dataMap[$item->getTableName()][$item->getId()][$fieldName] = implode(
+                ',',
+                array_values($desiredLocalizationIdMap)
+            );
+            return;
+        }
+
+        $localCommandMap = [];
+        foreach ($removeIds as $removeId) {
+            $localCommandMap[$foreignTableName][$removeId]['delete'] = true;
+        }
+        foreach ($removeAncestorIds as $removeAncestorId) {
+            $removeId = $dependentIdMap[$removeAncestorId];
+            $localCommandMap[$foreignTableName][$removeId]['delete'] = true;
+        }
+        foreach ($createAncestorIds as $createAncestorId) {
+            // if child table is not aware of localization, just copy
+            if (empty($languageFieldName) || empty($parentFieldName)) {
+                $localCommandMap[$foreignTableName][$createAncestorId]['copy'] = true;
+            // otherwise, trigger the localization process
+            } else {
+                $localCommandMap[$foreignTableName][$createAncestorId]['localize'] = $item->getLanguage();
+            }
+        }
+        // execute copy, localize and delete actions on persisted child records
+        if (!empty($localCommandMap)) {
+            $localDataHandler = GeneralUtility::makeInstance(DataHandler::class);
+            $localDataHandler->start([], $localCommandMap, $this->backendUser);
+            $localDataHandler->process_cmdmap();
+            // update copied or localized ids
+            foreach ($createAncestorIds as $createAncestorId) {
+                if (empty($localDataHandler->copyMappingArray[$foreignTableName][$createAncestorId])) {
+                    throw new \RuntimeException('Child record was not processed', 1486233164);
+                }
+                $newLocalizationId = $localDataHandler->copyMappingArray[$foreignTableName][$createAncestorId];
+                $newLocalizationId = $localDataHandler->getAutoVersionId($foreignTableName, $newLocalizationId) ?? $newLocalizationId;
+                $desiredLocalizationIdMap[$createAncestorId] = $newLocalizationId;
+            }
+        }
+        // populate new child records in data-map
+        if (!empty($populateAncestorIds)) {
+            foreach ($populateAncestorIds as $populateId) {
+                $newLocalizationId = StringUtility::getUniqueId('NEW');
+                $desiredLocalizationIdMap[$populateId] = $newLocalizationId;
+                // @todo l10n_mode=prefixLangTitle is not applied to this "in-memory translation"
+                $this->dataMap[$foreignTableName][$newLocalizationId] = $this->dataMap[$foreignTableName][$populateId];
+                $this->dataMap[$foreignTableName][$newLocalizationId][$languageFieldName] = $item->getLanguage();
+                // @todo Only $populatedIs used in TCA type 'select' is resolved in DataHandler's remapStack
+                $this->dataMap[$foreignTableName][$newLocalizationId][$parentFieldName] = $populateId;
+                if ($sourceFieldName !== null) {
+                    // @todo Not sure, whether $populateId is resolved in DataHandler's remapStack
+                    $this->dataMap[$foreignTableName][$newLocalizationId][$sourceFieldName] = $populateId;
+                }
+            }
+        }
+        // update inline parent field references - required to update pointer fields
+        $this->dataMap[$item->getTableName()][$item->getId()][$fieldName] = implode(
+            ',',
+            array_values($desiredLocalizationIdMap)
+        );
+    }
+
+    /**
+     * Fetches translation related field values for the items submitted in
+     * the data-map. That's why further adjustment for the tables pages vs.
+     * pages_language_overlay is not required.
+     *
+     * @param string $tableName
+     * @param array $fieldNames
+     * @param array $ids
+     * @return array
+     */
+    protected function fetchTranslationValues(string $tableName, array $fieldNames, array $ids)
+    {
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+            ->getQueryBuilderForTable($tableName);
+        $queryBuilder->getRestrictions()
+            ->removeAll()
+            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
+        $statement = $queryBuilder
+            ->select(...array_values($fieldNames))
+            ->from($tableName)
+            ->where(
+                $queryBuilder->expr()->in(
+                    'uid',
+                    $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
+                )
+            )
+            ->execute();
+
+        $translationValues = [];
+        foreach ($statement as $record) {
+            $translationValues[$record['uid']] = $record;
+        }
+        return $translationValues;
+    }
+
+    /**
+     * Create arary of dependent records
+     *
+     * @param string $tableName
+     * @param array $ids
+     * @return DataMapItem[][]
+     */
+    protected function fetchDependencies(string $tableName, array $ids)
+    {
+        if ($tableName === 'pages') {
+            $tableName = 'pages_language_overlay';
+        }
+
+        $fieldNames = [
+            'uid' => 'uid',
+            'l10n_state' => 'l10n_state',
+            'language' => $GLOBALS['TCA'][$tableName]['ctrl']['languageField'],
+            'parent' => $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'],
+        ];
+        if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['translationSource'])) {
+            $fieldNames['source'] = $GLOBALS['TCA'][$tableName]['ctrl']['translationSource'];
+        }
+
+        $dependentElements = $this->fetchDependentElements($tableName, $ids, $fieldNames);
+
+        $dependencyMap = [];
+        foreach ($dependentElements as $dependentElement) {
+            $dependentItem = DataMapItem::build(
+                $tableName,
+                $dependentElement['uid'],
+                [],
+                $dependentElement,
+                $fieldNames
+            );
+
+            if ($dependentItem->isDirectChildType()) {
+                $dependencyMap[$dependentItem->getParent()][State::STATE_PARENT][] = $dependentItem;
+            }
+            if ($dependentItem->isGrandChildType()) {
+                $dependencyMap[$dependentItem->getSource()][State::STATE_SOURCE][] = $dependentItem;
+            }
+        }
+        return $dependencyMap;
+    }
+
+    /**
+     * Fetch dependent records that depend on given record id's in their parent or source field and
+     * create an id map as further lookup array
+     *
+     * @param string $tableName
+     * @param array $ids
+     * @return array
+     */
+    protected function fetchDependentIdMap(string $tableName, array $ids)
+    {
+        if ($tableName === 'pages') {
+            $tableName = 'pages_language_overlay';
+        }
+
+        $fieldNames = [
+            'uid' => 'uid',
+            'l10n_state' => 'l10n_state',
+            'language' => $GLOBALS['TCA'][$tableName]['ctrl']['languageField'],
+            'parent' => $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'],
+        ];
+        if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['translationSource'])) {
+            $fieldNames['source'] = $GLOBALS['TCA'][$tableName]['ctrl']['translationSource'];
+        }
+
+        $dependentElements = $this->fetchDependentElements($tableName, $ids, $fieldNames);
+
+        $dependentIdMap = [];
+        foreach ($dependentElements as $dependentElement) {
+            // implicit: having source value different to parent value, use source pointer
+            if (
+                !empty($fieldNames['source'])
+                && $dependentElement[$fieldNames['source']] !== $dependentElement[$fieldNames['parent']]
+            ) {
+                $dependentIdMap[$dependentElement[$fieldNames['source']]] = $dependentElement['uid'];
+            // implicit: otherwise, use parent pointer
+            } else {
+                $dependentIdMap[$dependentElement[$fieldNames['parent']]] = $dependentElement['uid'];
+            }
+        }
+        return $dependentIdMap;
+    }
+
+
+    /**
+     * Fetch all elements that depend on given record id's in their parent or source field
+     *
+     * @param string $tableName
+     * @param array $ids
+     * @param array|null $fieldNames
+     * @return array
+     */
+    protected function fetchDependentElements(string $tableName, array $ids, array $fieldNames)
+    {
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+            ->getQueryBuilderForTable($tableName);
+        $queryBuilder->getRestrictions()
+            ->removeAll()
+            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
+
+        $predicates = [
+            $queryBuilder->expr()->in(
+                $fieldNames['parent'],
+                $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
+            )
+        ];
+
+        if (!empty($fieldNames['source'])) {
+            $predicates = [
+                $queryBuilder->expr()->in(
+                    $fieldNames['source'],
+                    $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
+                )
+            ];
+        }
+
+        $statement = $queryBuilder
+            ->select(...array_values($fieldNames))
+            ->from($tableName)
+            ->andWhere(
+                // must be any kind of localization
+                $queryBuilder->expr()->gt(
+                    $fieldNames['language'],
+                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
+                ),
+                // must be in connected mode
+                $queryBuilder->expr()->gt(
+                    $fieldNames['parent'],
+                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
+                ),
+                // any parent or source pointers
+                $queryBuilder->expr()->orX(...$predicates)
+            )
+            ->execute();
+
+        $dependentElements = [];
+        foreach ($statement as $record) {
+            $dependentElements[] = $record;
+        }
+        return $dependentElements;
+    }
+
+    /**
+     * Return array of data map items that are of given type
+     *
+     * @param string $type
+     * @return DataMapItem[]
+     */
+    protected function filterItemsByType(string $type)
+    {
+        return array_filter(
+            $this->items,
+            function(DataMapItem $item) use ($type) {
+                return $item->getType() === $type;
+            }
+        );
+    }
+
+    /**
+     * Return only id's that are integer - so no NEW...
+     *
+     * @param array $ids
+     * @param bool $numeric
+     * @return array
+     */
+    protected function filterNumericIds(array $ids, bool $numeric = true)
+    {
+        return array_filter(
+            $ids,
+            function($id) use ($numeric) {
+                return MathUtility::canBeInterpretedAsInteger($id) === $numeric;
+            }
+        );
+    }
+
+    /**
+     * Flatten array
+     *
+     * @param array $relationItems
+     * @return string[]
+     */
+    protected function mapRelationItemId(array $relationItems)
+    {
+        return array_map(
+            function(array $relationItem) {
+                return (string)$relationItem['id'];
+            },
+            $relationItems
+        );
+    }
+
+    /**
+     * See if an items is in item list and return it
+     *
+     * @param string $tableName
+     * @param string|int $id
+     * @return null|DataMapItem
+     */
+    protected function findItem(string $tableName, $id)
+    {
+        return $this->items[$tableName . ':' . $id] ?? null;
+    }
+
+    /**
+     * Field names we have to deal with
+     *
+     * @param DataMapItem $item
+     * @param string $scope
+     * @param null|bool $modified
+     * @return string[]
+     */
+    protected function getFieldNamesForItemScope(
+        DataMapItem $item,
+        string $scope,
+        bool $modified
+    ) {
+        if (
+            $scope === DataMapItem::SCOPE_PARENT
+            || $scope === DataMapItem::SCOPE_SOURCE
+        ) {
+            if (!State::isApplicable($item->getTableName())) {
+                return [];
+            }
+            return $item->getState()->filterFieldNames($scope, $modified);
+        }
+        if ($scope === DataMapItem::SCOPE_EXCLUDE) {
+            return $this->getLocalizationModeExcludeFieldNames(
+                $item->getTableName()
+            );
+        }
+        return [];
+    }
+
+    /**
+     * Field names of TCA table with columns having l10n_mode=exclude
+     *
+     * @param string $tableName
+     * @return string[]
+     */
+    protected function getLocalizationModeExcludeFieldNames(string $tableName)
+    {
+        $localizationExcludeFieldNames = [];
+        if (empty($GLOBALS['TCA'][$tableName]['columns'])) {
+            return $localizationExcludeFieldNames;
+        }
+
+        foreach ($GLOBALS['TCA'][$tableName]['columns'] as $fieldName => $configuration) {
+            if (($configuration['l10n_mode'] ?? null) === 'exclude') {
+                $localizationExcludeFieldNames[] = $fieldName;
+            }
+        }
+
+        return $localizationExcludeFieldNames;
+    }
+
+    /**
+     * True if we're dealing with a field that has foreign db relations
+     *
+     * @param string $tableName
+     * @param string $fieldName
+     * @return bool True if field is type=group with internalType === db or select with foreign_table
+     */
+    protected function isRelationField(string $tableName, string $fieldName): bool
+    {
+        if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']['type'])) {
+            return false;
+        }
+
+        $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
+
+        return (
+            $configuration['type'] === 'group'
+                && ($configuration['internal_type'] ?? null) === 'db'
+                && !empty($configuration['allowed'])
+            || $configuration['type'] === 'select'
+                && (
+                    !empty($configuration['foreign_table'])
+                        && !empty($GLOBALS['TCA'][$configuration['foreign_table']])
+                    || ($configuration['special'] ?? null) === 'languages'
+                )
+            || $this->isInlineRelationField($tableName, $fieldName)
+        );
+    }
+
+    /**
+     * True if we're dealing with an inline field
+     *
+     * @param string $tableName
+     * @param string $fieldName
+     * @return bool TRUE if field is of type inline with foreign_table set
+     */
+    protected function isInlineRelationField(string $tableName, string $fieldName): bool
+    {
+        if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']['type'])) {
+            return false;
+        }
+
+        $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
+
+        return (
+            $configuration['type'] === 'inline'
+            && !empty($configuration['foreign_table'])
+            && !empty($GLOBALS['TCA'][$configuration['foreign_table']])
+        );
+    }
+
+    /**
+     * Determines whether the table can be localized and either has fields
+     * with allowLanguageSynchronization enabled or l10n_mode set to exclude.
+     *
+     * @param string $tableName
+     * @return bool
+     */
+    protected function isApplicable(string $tableName): bool
+    {
+        return (
+            State::isApplicable($tableName)
+            || BackendUtility::isTableLocalizable($tableName)
+                && count($this->getLocalizationModeExcludeFieldNames($tableName)) > 0
+        );
+    }
+
+    /**
+     * @return RelationHandler
+     */
+    protected function createRelationHandler()
+    {
+        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
+        $relationHandler->setWorkspaceId($this->backendUser->workspace);
+        return $relationHandler;
+    }
+}
\ No newline at end of file
diff --git a/typo3/sysext/core/Classes/DataHandling/Localization/State.php b/typo3/sysext/core/Classes/DataHandling/Localization/State.php
new file mode 100644 (file)
index 0000000..93ed768
--- /dev/null
@@ -0,0 +1,296 @@
+<?php
+namespace TYPO3\CMS\Core\DataHandling\Localization;
+
+/*
+ * 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 TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Value object for l10n_state field value.
+ */
+class State
+{
+    const STATE_CUSTOM = 'custom';
+    const STATE_PARENT = 'parent';
+    const STATE_SOURCE = 'source';
+
+    /**
+     * @param string $tableName
+     * @return null|State
+     */
+    public static function create(string $tableName)
+    {
+        if (!static::isApplicable($tableName)) {
+            return null;
+        }
+
+        return GeneralUtility::makeInstance(
+            static::class,
+            $tableName
+        );
+    }
+
+    /**
+     * @param string $tableName
+     * @param string|null $json
+     * @return null|State
+     */
+    public static function fromJSON(string $tableName, string $json = null)
+    {
+        if (!static::isApplicable($tableName)) {
+            return null;
+        }
+
+        $states = json_decode($json ?? '', true);
+        return GeneralUtility::makeInstance(
+            static::class,
+            $tableName,
+            $states ?? []
+        );
+    }
+
+    /**
+     * @param string $tableName
+     * @return bool
+     */
+    public static function isApplicable(string $tableName)
+    {
+        return (
+            static::hasColumns($tableName)
+            && static::hasLanguageFieldName($tableName)
+            && static::hasTranslationParentFieldName($tableName)
+            && count(static::getFieldNames($tableName)) > 0
+        );
+    }
+
+    /**
+     * @param string $tableName
+     * @return bool
+     */
+    protected static function hasColumns(string $tableName)
+    {
+        return (
+            !empty($GLOBALS['TCA'][$tableName]['columns'])
+            && is_array($GLOBALS['TCA'][$tableName]['columns'])
+        );
+    }
+
+    /**
+     * @param string $tableName
+     * @return bool
+     */
+    protected static function hasLanguageFieldName(string $tableName)
+    {
+        return !empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField']);
+    }
+
+    /**
+     * @param string $tableName
+     * @return bool
+     */
+    protected static function hasTranslationParentFieldName(string $tableName)
+    {
+        return !empty($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']);
+    }
+
+    /**
+     * @param string $tableName
+     * @return array
+     */
+    protected static function getFieldNames(string $tableName)
+    {
+        return array_keys(
+            array_filter(
+                $GLOBALS['TCA'][$tableName]['columns'],
+                function(array $fieldConfiguration) {
+                    return !empty(
+                        $fieldConfiguration['config']
+                            ['behaviour']['allowLanguageSynchronization']
+                    );
+                }
+            )
+        );
+    }
+
+    /**
+     * @var string
+     */
+    protected $tableName;
+
+    /**
+     * @var array
+     */
+    protected $states;
+
+    /**
+     * @var array
+     */
+    protected $originalStates;
+
+    /**
+     * @param string $tableName
+     * @param array $states
+     */
+    public function __construct(string $tableName, array $states = array())
+    {
+        $this->tableName = $tableName;
+        $this->states = $states;
+        $this->originalStates = $states;
+
+        $this->states = $this->sanitize($states);
+        $this->states = $this->enrich($states);
+    }
+
+    /**
+     * @param array $states
+     */
+    public function update(array $states)
+    {
+        $this->states = array_merge(
+            $this->states,
+            $this->sanitize($states)
+        );
+    }
+
+    /**
+     * @return string|null
+     */
+    public function export()
+    {
+        if (empty($this->states)) {
+            return null;
+        }
+        return json_encode($this->states);
+    }
+
+    /**
+     * @return string[]
+     */
+    public function getModifiedFieldNames()
+    {
+        return array_keys(
+            array_diff_assoc(
+                $this->states,
+                $this->originalStates
+            )
+        );
+    }
+
+    /**
+     * @return bool
+     */
+    public function isModified()
+    {
+        return !empty($this->getModifiedFieldNames());
+    }
+
+    /**
+     * @param string $fieldName
+     * @return bool
+     */
+    public function isUndefined(string $fieldName)
+    {
+        return !isset($this->states[$fieldName]);
+    }
+
+    /**
+     * @param string $fieldName
+     * @return bool
+     */
+    public function isCustomState(string $fieldName)
+    {
+        return ($this->states[$fieldName] ?? null) === static::STATE_CUSTOM;
+    }
+
+    /**
+     * @param string $fieldName
+     * @return bool
+     */
+    public function isParentState(string $fieldName)
+    {
+        return ($this->states[$fieldName] ?? null) === static::STATE_PARENT;
+    }
+
+    /**
+     * @param string $fieldName
+     * @return bool
+     */
+    public function isSourceState(string $fieldName)
+    {
+        return ($this->states[$fieldName] ?? null) === static::STATE_SOURCE;
+    }
+
+    /**
+     * @param string $fieldName
+     * @return null|string
+     */
+    public function getState(string $fieldName)
+    {
+        return ($this->states[$fieldName] ?? null);
+    }
+
+    /**
+     * Filters field names having a desired state.
+     *
+     * @param string $desiredState
+     * @param bool $modified
+     * @return string[]
+     */
+    public function filterFieldNames(string $desiredState, bool $modified = false)
+    {
+        if (!$modified) {
+            $fieldNames = array_keys($this->states);
+        } else {
+            $fieldNames = $this->getModifiedFieldNames();
+        }
+        return array_filter(
+            $fieldNames,
+            function($fieldName) use ($desiredState) {
+                return $this->states[$fieldName] === $desiredState;
+            }
+        );
+    }
+
+    /**
+     * Filter out field names that don't exist in TCA.
+     *
+     * @param array $states
+     * @return array
+     */
+    protected function sanitize(array $states)
+    {
+        $fieldNames = static::getFieldNames($this->tableName);
+        return array_intersect_key(
+            $states,
+            array_combine($fieldNames, $fieldNames)
+        );
+    }
+
+    /**
+     * Add missing states for field names.
+     *
+     * @param array $states
+     * @return array
+     */
+    protected function enrich(array $states)
+    {
+        foreach (static::getFieldNames($this->tableName) as $fieldName) {
+            if (!empty($states[$fieldName])) {
+                continue;
+            }
+            $states[$fieldName] = static::STATE_PARENT;
+        }
+        return $states;
+    }
+}
\ No newline at end of file
index 0a66f02..115f7c8 100644 (file)
@@ -928,8 +928,13 @@ class TcaMigration
                 }
                 if ($fieldConfig['l10n_mode'] === 'mergeIfNotBlank') {
                     unset($fieldConfig['l10n_mode']);
+                    if (empty($fieldConfig['config']['behaviour']['allowLanguageSynchronization'])) {
+                        $fieldConfig['config']['behaviour']['allowLanguageSynchronization'] = true;
+                    }
                     $this->messages[] = 'The TCA setting \'mergeIfNotBlank\' was removed '
-                        . 'in TCA ' . $table . '[\'columns\'][\'' . $fieldName . '\'][\'l10n_mode\']';
+                        . 'in TCA ' . $table . '[\'columns\'][\'' . $fieldName . '\'][\'l10n_mode\']'
+                        . ' and changed to ' . $table . '[\'columns\'][\'' . $fieldName . '\'][\'behaviour\']'
+                        . '[\'allowLanguageSynchronization\'] = true';
                 }
             }
         }
index 721fbbb..a3a1599 100644 (file)
@@ -116,7 +116,10 @@ return [
                 'type' => 'input',
                 'renderType' => 'inputDateTime',
                 'eval' => 'datetime',
-                'default' => 0
+                'default' => 0,
+                'behaviour' => [
+                    'allowLanguageSynchronization' => true,
+                ]
             ]
         ],
         'endtime' => [
@@ -129,6 +132,9 @@ return [
                 'default' => 0,
                 'range' => [
                     'upper' => mktime(0, 0, 0, 1, 1, 2038),
+                ],
+                'behaviour' => [
+                    'allowLanguageSynchronization' => true,
                 ]
             ]
         ],
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-51291-PageRepositoryShouldFieldBeOverlaid.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-51291-PageRepositoryShouldFieldBeOverlaid.rst
new file mode 100644 (file)
index 0000000..1bcfd1b
--- /dev/null
@@ -0,0 +1,34 @@
+.. include:: ../../Includes.txt
+
+============================================================
+Deprecation: #51291 - PageRepository shouldFieldBeOverlaid()
+============================================================
+
+See :issue:`51291`
+
+Description
+===========
+
+The following method has been deprecated:
+
+* :code:`TYPO3\CMS\Frontend\Page\PageRepository->shouldFieldBeOverlaid()`
+
+
+Impact
+======
+
+Localized record fields are always "overlaid", the method returns true in all cases.
+
+
+Affected Installations
+======================
+
+Instances with extensions calling this method
+
+
+Migration
+=========
+
+The deprecated method returns TRUE in all cases, the call can be omitted.
+
+.. index:: Frontend, PHP-API
\ No newline at end of file
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-51291-SynchronizedFieldValuesInLocalizedRecords.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-51291-SynchronizedFieldValuesInLocalizedRecords.rst
new file mode 100644 (file)
index 0000000..f938d28
--- /dev/null
@@ -0,0 +1,46 @@
+.. include:: ../../Includes.txt
+
+================================================================
+Feature: #51291 - Synchronized field values in localized records
+================================================================
+
+See :issue:`51291`
+
+Description
+===========
+
+The localized record overlay behaviour has been changed to make localization rows standalone.
+
+Previously, if fields in :code:`TCA` columns were set to :code:`l10n_mode` :code:`exclude`
+or :code:`mergeIfNotBlank`, the localized record overlay did not contain values, and those
+values were "pulled up" from the underlying default language records.
+
+This has been changed, the :code:`DataHandler` now copies those values over to the localized
+record and synchronizes them if the default language record is changed.
+
+As a substitution of the :code:`mergeIfNotBlank` feature, the new configuration :code:`allowLanguageSynchronization`
+has been added. Setting this adds a wizard to single fields and an editor can select if a field of a localized record
+should be kept in sync with the default language record, or the localized record it was derived from.
+
+A typical configuration looks like that:
+
+.. code-block:: php
+
+    'columns' => [
+        ...
+        'header' => [
+            'label' => 'My header',
+            'config' => [
+                'type' => 'input',
+                'behaviour' => [
+                    'allowLanguageSynchronization' => true,
+                ],
+            ],
+        ],
+    ],
+
+:code:`TCA` tables that configure the language localization get field :code:`l10n_state` added by the schema analyzer
+which stores an json array with field names and the values :code:`custom`, :code:`parent` or :code:`source` to
+specify if and from which record a single field gets its value.
+
+.. index:: Backend, Database, Frontend, PHP-API, TCA
\ No newline at end of file
index b0950f8..b091ed9 100644 (file)
@@ -217,6 +217,16 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D
         $this->recordIds['localizedContentId'] = $newTableIds[self::TABLE_Content][self::VALUE_ContentIdLast];
     }
 
+    public function localizeContentOfRelationWithLanguageSynchronization()
+    {
+        $GLOBALS['TCA']['tt_content']['columns']['tx_testdatahandler_group']['config']['behaviour']['allowLanguageSynchronization'] = true;
+        $newTableIds = $this->actionService->localizeRecord(self::TABLE_Content, self::VALUE_ContentIdLast, self::VALUE_LanguageId);
+        $this->recordIds['localizedContentId'] = $newTableIds[self::TABLE_Content][self::VALUE_ContentIdLast];
+        $this->actionService->modifyReferences(
+            self::TABLE_Content, self::VALUE_ContentIdLast, self::FIELD_ContentElement, [self::VALUE_ElementIdFirst, self::VALUE_ElementIdSecond]
+        );
+    }
+
     /**
      * @test
      * @see DataSet/localizeElementOfRelation.csv
index 658b407..98d197a 100644 (file)
@@ -288,6 +288,21 @@ class ActionTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\Group\Abs
 
     /**
      * @test
+     * @see DataSet/localizeContentOfRelationWSynchronization.csv
+     */
+    public function localizeContentOfRelationWithLanguageSynchronization()
+    {
+        parent::localizeContentOfRelationWithLanguageSynchronization();
+        $this->assertAssertionDataSet('localizeContentOfRelationWSynchronization');
+
+        $responseSections = $this->getFrontendResponse(self::VALUE_PageId, self::VALUE_LanguageId)->getResponseSections();
+        $this->assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
+            ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdLast)->setRecordField(self::FIELD_ContentElement)
+            ->setTable(self::TABLE_Element)->setField('title')->setValues('Element #1', 'Element #2'));
+    }
+
+    /**
+     * @test
      * @see DataSet/localizeElementOfRelation.csv
      */
     public function localizeElementOfRelation()
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Group/Modify/DataSet/localizeContentOfRelationWSynchronization.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Group/Modify/DataSet/localizeContentOfRelationWSynchronization.csv
new file mode 100644 (file)
index 0000000..f52954f
--- /dev/null
@@ -0,0 +1,16 @@
+"pages",,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title",,,
+,1,0,256,0,0,0,0,0,0,0,"FunctionalTest",,,
+,88,1,256,0,0,0,0,0,0,0,"DataHandlerTest",,,
+,89,88,256,0,0,0,0,0,0,0,"Relations",,,
+,90,88,512,0,0,0,0,0,0,0,"Target",,,
+"tt_content",,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","header","tx_testdatahandler_group"
+,297,89,256,0,0,0,0,0,0,0,0,0,"Regular Element #1","1,2"
+,298,89,512,0,0,0,0,0,0,0,0,0,"Regular Element #2","1,2"
+,299,89,768,0,1,298,298,0,0,0,0,0,"[Translate to Dansk:] Regular Element #2","1,2"
+"tx_testdatahandler_element",,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l10n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title",
+,1,89,256,0,0,0,0,0,0,0,0,0,"Element #1",
+,2,89,512,0,0,0,0,0,0,0,0,0,"Element #2",
+,3,89,768,0,0,0,0,0,0,0,0,0,"Element #3",
index 586ee67..4d75bc6 100644 (file)
@@ -194,6 +194,23 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D
         $this->recordIds['localizedContentId'] = $newTableIds[self::TABLE_Content][self::VALUE_ContentIdLast];
     }
 
+    public function localizeParentContentWithAllChildrenInSelectModeAndLanguageSynchronization()
+    {
+        $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['localizationMode'] = 'select';
+        $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['allowLanguageSynchronization'] = true;
+        $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['localizeChildrenAtParentLocalization'] = true;
+        $GLOBALS['TCA'][self::TABLE_Hotel]['columns'][self::FIELD_HotelOffer]['config']['behaviour']['localizeChildrenAtParentLocalization'] = true;
+        $newTableIds = $this->actionService->localizeRecord(self::TABLE_Content, self::VALUE_ContentIdLast, self::VALUE_LanguageId);
+        $this->recordIds['localizedContentId'] = $newTableIds[self::TABLE_Content][self::VALUE_ContentIdLast];
+        $this->actionService->modifyRecords(
+            self::VALUE_PageId,
+            [
+                self::TABLE_Content => ['uid' => self::VALUE_ContentIdLast, self::FIELD_ContentHotel => '5,__nextUid'],
+                self::TABLE_Hotel => ['uid' => '__NEW', 'title' => 'Hotel #2'],
+            ]
+        );
+    }
+
     /**
      * @see DataSet/changeParentContentRecordSorting.csv
      */
index e1e215b..02255f6 100644 (file)
@@ -229,6 +229,22 @@ class ActionTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\IRRE\CSV\
 
     /**
      * @test
+     * @see DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv
+     */
+    public function localizeParentContentWithAllChildrenInSelectModeAndLanguageSynchronization()
+    {
+        parent::localizeParentContentWithAllChildrenInSelectModeAndLanguageSynchronization();
+        $this->assertAssertionDataSet('localizeParentContentWAllChildrenSelectNLanguageSynchronization');
+
+        $responseSections = $this->getFrontendResponse(self::VALUE_PageId, self::VALUE_LanguageId)->getResponseSections();
+        $this->assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
+            ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdLast)->setRecordField(self::FIELD_ContentHotel)
+            // @todo Actually Hotel #2 should be prefixed as well
+            ->setTable(self::TABLE_Hotel)->setField('title')->setValues('[Translate to Dansk:] Hotel #1', 'Hotel #2'));
+    }
+
+    /**
+     * @test
      * @see DataSet/changeParentContentRecordSorting.csv
      */
     public function changeParentContentSorting()
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/CSV/Modify/DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/CSV/Modify/DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv
new file mode 100644 (file)
index 0000000..4c3d1c0
--- /dev/null
@@ -0,0 +1,30 @@
+"tt_content",,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","header","tx_irretutorial_1ncsv_hotels"
+,297,89,256,0,0,0,0,0,0,0,0,0,"Regular Element #1","3,4"
+,298,89,512,0,0,0,0,0,0,0,0,0,"Regular Element #2","5,7"
+,299,89,768,0,1,298,298,0,0,0,0,0,"[Translate to Dansk:] Regular Element #2","6,8"
+"tx_irretutorial_1ncsv_hotel",,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","offers"
+,3,89,256,0,0,0,0,0,0,0,0,0,"Hotel #1","5,6"
+,4,89,128,0,0,0,0,0,0,0,0,0,"Hotel #2",7
+,5,89,64,0,0,0,0,0,0,0,0,0,"Hotel #1",8
+,6,89,96,0,1,5,5,0,0,0,0,0,"[Translate to Dansk:] Hotel #1",9
+,7,89,32,0,0,0,0,0,0,0,0,0,"Hotel #2",""
+,8,89,16,0,1,7,0,0,0,0,0,0,"Hotel #2",""
+"tx_irretutorial_1ncsv_offer",,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","prices"
+,5,89,256,0,0,0,0,0,0,0,0,0,"Offer #1.1","7,8,9"
+,6,89,128,0,0,0,0,0,0,0,0,0,"Offer #1.2","10,11"
+,7,89,64,0,0,0,0,0,0,0,0,0,"Offer #2.1",12
+,8,89,32,0,0,0,0,0,0,0,0,0,"Offer #1.1",13
+,9,89,48,0,1,8,8,0,0,0,0,0,"[Translate to Dansk:] Offer #1.1",14
+"tx_irretutorial_1ncsv_price",,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title",
+,7,89,256,0,0,0,0,0,0,0,0,0,"Price #1.1.1",
+,8,89,128,0,0,0,0,0,0,0,0,0,"Price #1.1.2",
+,9,89,64,0,0,0,0,0,0,0,0,0,"Price #1.1.3",
+,10,89,32,0,0,0,0,0,0,0,0,0,"Price #1.2.1",
+,11,89,16,0,0,0,0,0,0,0,0,0,"Price #1.2.2",
+,12,89,8,0,0,0,0,0,0,0,0,0,"Price #2.1.1",
+,13,89,4,0,0,0,0,0,0,0,0,0,"Price #1.1.1",
+,14,89,6,0,1,13,13,0,0,0,0,0,"[Translate to Dansk:] Price #1.1.1",
index 9ebcea4..81b52e0 100644 (file)
@@ -201,6 +201,26 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D
     }
 
     /**
+     * @see DataSet/localizeParentContentWAllChildrenSelect.csv
+     */
+    public function localizeParentContentWithAllChildrenInSelectModeAndLanguageSynchronization()
+    {
+        $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['localizationMode'] = 'select';
+        $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['allowLanguageSynchronization'] = true;
+        $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['localizeChildrenAtParentLocalization'] = true;
+        $GLOBALS['TCA'][self::TABLE_Hotel]['columns'][self::FIELD_HotelOffer]['config']['behaviour']['localizeChildrenAtParentLocalization'] = true;
+        $newTableIds = $this->actionService->localizeRecord(self::TABLE_Content, self::VALUE_ContentIdLast, self::VALUE_LanguageId);
+        $this->recordIds['localizedContentId'] = $newTableIds[self::TABLE_Content][self::VALUE_ContentIdLast];
+        $this->actionService->modifyRecords(
+            self::VALUE_PageId,
+            [
+                self::TABLE_Content => ['uid' => self::VALUE_ContentIdLast, self::FIELD_ContentHotel => '5,__nextUid'],
+                self::TABLE_Hotel => ['uid' => '__NEW', 'title' => 'Hotel #2'],
+            ]
+        );
+    }
+
+    /**
      * @see DataSet/changeParentContentRecordSorting.csv
      */
     public function changeParentContentSorting()
index 0b19920..ff28c1a 100644 (file)
@@ -230,6 +230,22 @@ class ActionTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\IRRE\Fore
 
     /**
      * @test
+     * @see DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv
+     */
+    public function localizeParentContentWithAllChildrenInSelectModeAndLanguageSynchronization()
+    {
+        parent::localizeParentContentWithAllChildrenInSelectModeAndLanguageSynchronization();
+        $this->assertAssertionDataSet('localizeParentContentWAllChildrenSelectNLanguageSynchronization');
+
+        $responseSections = $this->getFrontendResponse(self::VALUE_PageId, self::VALUE_LanguageId)->getResponseSections('Default', 'Extbase:list()');
+        $this->assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
+            ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdLast)->setRecordField(self::FIELD_ContentHotel)
+            // @todo Actually Hotel #2 should be prefixed as well
+            ->setTable(self::TABLE_Hotel)->setField('title')->setValues('[Translate to Dansk:] Hotel #1', 'Hotel #2'));
+    }
+
+    /**
+     * @test
      * @see DataSet/changeParentContentRecordSorting.csv
      */
     public function changeParentContentSorting()
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv
new file mode 100644 (file)
index 0000000..3905471
--- /dev/null
@@ -0,0 +1,30 @@
+"tt_content",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","header","tx_irretutorial_1nff_hotels",,,
+,297,89,256,0,0,0,0,0,0,0,0,0,"Regular Element #1",2,,,
+,298,89,512,0,0,0,0,0,0,0,0,0,"Regular Element #2",2,,,
+,299,89,768,0,1,298,298,0,0,0,0,0,"[Translate to Dansk:] Regular Element #2",2,,,
+"tx_irretutorial_1nff_hotel",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","parentid","parenttable","parentidentifier","offers"
+,3,89,1024,0,0,0,0,0,0,0,0,0,"Hotel #1",297,"tt_content",,2
+,4,89,1536,0,0,0,0,0,0,0,0,0,"Hotel #2",297,"tt_content",,1
+,5,89,1,0,0,0,0,0,0,0,0,0,"Hotel #1",298,"tt_content",,1
+,6,89,1,0,1,5,5,0,0,0,0,0,"[Translate to Dansk:] Hotel #1",299,"tt_content",,1
+,7,89,2,0,0,0,0,0,0,0,0,0,"Hotel #2",298,"tt_content",,0
+,8,89,2,0,1,7,0,0,0,0,0,0,"Hotel #2",299,"tt_content",,0
+"tx_irretutorial_1nff_offer",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","parentid","parenttable","parentidentifier","prices"
+,5,89,512,0,0,0,0,0,0,0,0,0,"Offer #1.1",3,"tx_irretutorial_1nff_hotel",,3
+,6,89,1536,0,0,0,0,0,0,0,0,0,"Offer #1.2",3,"tx_irretutorial_1nff_hotel",,2
+,7,89,768,0,0,0,0,0,0,0,0,0,"Offer #2.1",4,"tx_irretutorial_1nff_hotel",,1
+,8,89,1024,0,0,0,0,0,0,0,0,0,"Offer #1.1",5,"tx_irretutorial_1nff_hotel",,1
+,9,89,1,0,1,8,8,0,0,0,0,0,"[Translate to Dansk:] Offer #1.1",6,"tx_irretutorial_1nff_hotel",,1
+"tx_irretutorial_1nff_price",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","parentid","parenttable","parentidentifier",
+,7,89,512,0,0,0,0,0,0,0,0,0,"Price #1.1.1",5,"tx_irretutorial_1nff_offer",,
+,8,89,1792,0,0,0,0,0,0,0,0,0,"Price #1.1.2",5,"tx_irretutorial_1nff_offer",,
+,9,89,2304,0,0,0,0,0,0,0,0,0,"Price #1.1.3",5,"tx_irretutorial_1nff_offer",,
+,10,89,768,0,0,0,0,0,0,0,0,0,"Price #1.2.1",6,"tx_irretutorial_1nff_offer",,
+,11,89,2048,0,0,0,0,0,0,0,0,0,"Price #1.2.2",6,"tx_irretutorial_1nff_offer",,
+,12,89,1024,0,0,0,0,0,0,0,0,0,"Price #2.1.1",7,"tx_irretutorial_1nff_offer",,
+,13,89,1280,0,0,0,0,0,0,0,0,0,"Price #1.1.1",8,"tx_irretutorial_1nff_offer",,
+,14,89,1,0,1,13,13,0,0,0,0,0,"[Translate to Dansk:] Price #1.1.1",9,"tx_irretutorial_1nff_offer",,
index 51837c5..f487ff3 100644 (file)
@@ -137,6 +137,13 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D
         $this->actionService->localizeRecord(self::TABLE_Content, self::VALUE_ContentIdSecond, self::VALUE_LanguageId);
     }
 
+    public function localizeContentWithLanguageSynchronization()
+    {
+        $GLOBALS['TCA']['tt_content']['columns']['header']['config']['behaviour']['allowLanguageSynchronization'] = true;
+        $this->actionService->localizeRecord(self::TABLE_Content, self::VALUE_ContentIdSecond, self::VALUE_LanguageId);
+        $this->actionService->modifyRecord(self::TABLE_Content, self::VALUE_ContentIdSecond, ['header' => 'Testing #1']);
+    }
+
     /**
      * @test
      * @see DataSet/localizeContentFromNonDefaultLanguage.csv
index 37a38e0..25e13f9 100644 (file)
@@ -166,6 +166,20 @@ class ActionTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\Regular\A
 
     /**
      * @test
+     * @see DataSet/localizeContentWSynchronization.csv
+     */
+    public function localizeContentWithLanguageSynchronization()
+    {
+        parent::localizeContentWithLanguageSynchronization();
+        $this->assertAssertionDataSet('localizeContentWSynchronization');
+
+        $responseSections = $this->getFrontendResponse(self::VALUE_PageId, self::VALUE_LanguageId)->getResponseSections();
+        $this->assertThat($responseSections, $this->getRequestSectionHasRecordConstraint()
+            ->setTable(self::TABLE_Content)->setField('header')->setValues('[Translate to Dansk:] Regular Element #1', 'Testing #1'));
+    }
+
+    /**
+     * @test
      * @see DataSet/localizeContentFromNonDefaultLanguage.csv
      */
     public function localizeContentFromNonDefaultLanguage()
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/localizeContentWSynchronization.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/localizeContentWSynchronization.csv
new file mode 100644 (file)
index 0000000..741cfaa
--- /dev/null
@@ -0,0 +1,9 @@
+tt_content,,,,,,,,,,,,,,
+,uid,pid,sorting,deleted,sys_language_uid,l18n_parent,l10n_source,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,header
+,297,89,256,0,0,0,0,0,0,0,0,0,0,Regular Element #1
+,298,89,512,0,0,0,0,0,0,0,0,0,0,Testing #1
+,299,89,768,0,0,0,0,0,0,0,0,0,0,Regular Element #3
+,300,89,1024,0,1,299,299,299,0,0,0,0,0,[Translate to Dansk:] Regular Element #3
+,301,89,384,0,1,297,297,297,0,0,0,0,0,[Translate to Dansk:] Regular Element #1
+,302,89,448,0,2,297,301,301,0,0,0,0,0,[Translate to Deutsch:] [Translate to Dansk:] Regular Element #1
+,303,89,416,0,1,298,298,298,0,0,0,0,0,Testing #1
index 306ed0e..5d58d72 100644 (file)
@@ -2121,6 +2121,11 @@ class TcaMigrationTest extends \TYPO3\Components\TestingFramework\Core\Unit\Unit
                     'aTable' => [
                         'columns' => [
                             'aColumn' => [
+                                'config' => [
+                                    'behaviour' => [
+                                        'allowLanguageSynchronization' => true,
+                                    ]
+                                ]
                             ],
                         ],
                     ],
index 1f8091c..22ce307 100644 (file)
@@ -69,6 +69,14 @@ $signalSlotDispatcher->connect(
     'processFile'
 );
 
+$signalSlotDispatcher->connect(
+    \TYPO3\CMS\Install\Service\SqlExpectedSchemaService::class,
+    'tablesDefinitionIsBeingBuilt',
+    \TYPO3\CMS\Core\DataHandling\DatabaseSchemaService::class,
+    'getLocalizationRequiredDatabaseSchema'
+);
+
+
 unset($signalSlotDispatcher);
 
 $GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include']['dumpFile'] = \TYPO3\CMS\Core\Controller\FileDumpController::class . '::dumpAction';
index b331000..3b15749 100644 (file)
@@ -279,7 +279,10 @@ $tca = [
             'config' => [
                 'type' => 'input',
                 'size' => 20,
-                'eval' => 'trim'
+                'eval' => 'trim',
+                'behaviour' => [
+                    'allowLanguageSynchronization' => true,
+                ]
             ],
         ],
         'location_region' => [
@@ -289,7 +292,10 @@ $tca = [
             'config' => [
                 'type' => 'input',
                 'size' => 20,
-                'eval' => 'trim'
+                'eval' => 'trim',
+                'behaviour' => [
+                    'allowLanguageSynchronization' => true,
+                ]
             ],
         ],
         'location_city' => [
@@ -299,7 +305,10 @@ $tca = [
             'config' => [
                 'type' => 'input',
                 'size' => 20,
-                'eval' => 'trim'
+                'eval' => 'trim',
+                'behaviour' => [
+                    'allowLanguageSynchronization' => true,
+                ]
             ],
         ],
         'latitude' => [
index 1ff9ef5..0098bf7 100644 (file)
@@ -506,9 +506,7 @@ class PageRepository
                     // Overwrite the original field with the overlay
                     foreach ($overlays[$origPage['uid']] as $fieldName => $fieldValue) {
                         if ($fieldName !== 'uid' && $fieldName !== 'pid') {
-                            if ($this->shouldFieldBeOverlaid('pages_language_overlay', $fieldName, $fieldValue)) {
-                                $pagesOutput[$key][$fieldName] = $fieldValue;
-                            }
+                            $pagesOutput[$key][$fieldName] = $fieldValue;
                         }
                     }
                 }
@@ -594,9 +592,7 @@ class PageRepository
                                 }
                                 foreach ($row as $fN => $fV) {
                                     if ($fN !== 'uid' && $fN !== 'pid' && isset($olrow[$fN])) {
-                                        if ($this->shouldFieldBeOverlaid($table, $fN, $olrow[$fN])) {
-                                            $row[$fN] = $olrow[$fN];
-                                        }
+                                        $row[$fN] = $olrow[$fN];
                                     } elseif ($fN === 'uid') {
                                         $row['_LOCALIZED_UID'] = $olrow['uid'];
                                     }
@@ -1889,10 +1885,7 @@ class PageRepository
         );
         if ($isTableLocalizable && $localizedId !== null) {
             $localizedReferences = $fileRepository->findByRelation($tableName, $fieldName, $localizedId);
-            $localizedReferencesValue = $localizedReferences ?: '';
-            if ($this->shouldFieldBeOverlaid($tableName, $fieldName, $localizedReferencesValue)) {
-                $references = $localizedReferences;
-            }
+            $references = $localizedReferences;
         }
 
         return $references;
@@ -1922,20 +1915,12 @@ class PageRepository
      * @param string $field TCA fieldname
      * @param mixed $value Current value of the field
      * @return bool Returns TRUE if a given record field needs to be overlaid
+     * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
      */
     protected function shouldFieldBeOverlaid($table, $field, $value)
     {
-        $l10n_mode = isset($GLOBALS['TCA'][$table]['columns'][$field]['l10n_mode'])
-            ? $GLOBALS['TCA'][$table]['columns'][$field]['l10n_mode']
-            : '';
-
-        $shouldFieldBeOverlaid = true;
-
-        if ($l10n_mode === 'exclude') {
-            $shouldFieldBeOverlaid = false;
-        }
-
-        return $shouldFieldBeOverlaid;
+        GeneralUtility::logDeprecatedFunction();
+        return true;
     }
 
     /**
index cd31a85..33d74cd 100644 (file)
@@ -466,8 +466,8 @@ return [
             'config' => [
                 'type' => 'input',
                 'size' => 50,
-                'max' => 255
-            ]
+                'max' => 255,
+            ],
         ],
         'header_layout' => [
             'exclude' => true,
@@ -971,7 +971,7 @@ return [
             'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig('media', [
                 'appearance' => [
                     'createNewRelationLinkTitle' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:media.addFileReference'
-                ]
+                ],
             ])
         ],
         'filelink_size' => [
index 28770c9..6ff7ad6 100644 (file)
@@ -126,11 +126,11 @@ class PageRepositoryTest extends \TYPO3\Components\TestingFramework\Core\Unit\Un
     public function getShouldFieldBeOverlaidData()
     {
         return [
-            ['default',               'fake_table', 'foobar', true,  'default is to merge non-empty string'],
-            ['default',               'fake_table', '',       true,  'default is to merge empty string'],
+            ['default',               'fake_table', 'foobar', true,  'default is to overlay non-empty string'],
+            ['default',               'fake_table', '',       true,  'default is to overlay empty string'],
 
-            ['exclude',               'fake_table', '',       false, 'exclude field with empty string'],
-            ['exclude',               'fake_table', 'foobar', false, 'exclude field with non-empty string'],
+            ['exclude',               'fake_table', '',       true, 'exclude field with empty string'],
+            ['exclude',               'fake_table', 'foobar', true, 'exclude field with non-empty string'],
 
             ['prefixLangTitle',       'fake_table', 'foobar', true,  'prefixLangTitle is merged with non-empty string'],
             ['prefixLangTitle',       'fake_table', '',       true,  'prefixLangTitle is merged with empty string'],
index d922717..eaa9da2 100644 (file)
@@ -48,7 +48,7 @@ class DatabaseRowsUpdateWizard extends AbstractUpdate
      * @var array Single classes that may update rows
      */
     protected $rowUpdater = [
-        L10nModeUpdater::class,
+//        L10nModeUpdater::class,
     ];
 
     /**
index 7383b58..ee99575 100644 (file)
@@ -17,7 +17,6 @@ namespace TYPO3\CMS\Install\Updates\RowUpdater;
 
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Database\ConnectionPool;
-use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
 use TYPO3\CMS\Core\DataHandling\DataHandler;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Install\Service\LoadTcaService;
@@ -25,27 +24,12 @@ use TYPO3\CMS\Install\Service\LoadTcaService;
 /**
  * Migrate values for database records having columns
  * using "l10n_mode" set to "mergeIfNotBlank".
+ *
+ * @todo: This needs a review and finish
  */
 class L10nModeUpdater implements RowUpdaterInterface
 {
     /**
-     * Field names that previously had a migrated l10n_mode setting in TCA.
-     *
-     * @var array
-     */
-    protected $migratedL10nCoreFieldNames = [
-        'sys_category' => [
-            'starttime' => 'mergeIfNotBlank',
-            'endtime' => 'mergeIfNotBlank',
-        ],
-        'sys_file_metadata' => [
-            'location_country' => 'mergeIfNotBlank',
-            'location_region' => 'mergeIfNotBlank',
-            'location_city' => 'mergeIfNotBlank',
-        ],
-    ];
-
-    /**
      * List of tables with information about to migrate fields.
      * Created during hasPotentialUpdateForTable(), used in updateTableRow()
      *
@@ -60,7 +44,8 @@ class L10nModeUpdater implements RowUpdaterInterface
      */
     public function getTitle(): string
     {
-        return 'Migrate values in database records having "l10n_mode" set to "mergeIfNotBlank';
+        return 'Migrate values in database records having "l10n_mode"'
+            . ' either set to "exclude" or "mergeIfNotBlank"';
     }
 
     /**
@@ -102,6 +87,7 @@ class L10nModeUpdater implements RowUpdaterInterface
         $fakeAdminUser = GeneralUtility::makeInstance(BackendUserAuthentication::class);
         $fakeAdminUser->user = ['admin' => 1];
 
+        // disable DataHandler hooks for processing this update
         if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php'])) {
             $dataHandlerHooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php'];
             unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']);
@@ -119,6 +105,7 @@ class L10nModeUpdater implements RowUpdaterInterface
         $sourceRow = $this->getRow($sourceTableName, $source);
 
         $updateValues = [];
+        $l10nState = [];
 
         $row = $this->getRow($tableName, $uid);
         foreach ($row as $fieldName => $fieldValue) {
@@ -126,6 +113,8 @@ class L10nModeUpdater implements RowUpdaterInterface
                 continue;
             }
 
+            $l10nState[$fieldName] = 'custom';
+
             if (
                 // default
                 empty($fieldTypes[$fieldName])
@@ -139,6 +128,7 @@ class L10nModeUpdater implements RowUpdaterInterface
                 )
             ) {
                 $updateValues[$fieldName] = $sourceRow[$fieldName];
+                $l10nState[$fieldName] = 'parent';
             }
             // inline types, but only file references
             if (
@@ -161,12 +151,11 @@ class L10nModeUpdater implements RowUpdaterInterface
                 $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
                 $dataHandler->start([], $commandMap, $fakeAdminUser);
                 $dataHandler->process_cmdmap();
+                $l10nState[$fieldName] = 'parent';
             }
         }
 
-        if (empty($updateValues)) {
-            return $inputRow;
-        }
+        $updateValues['l10n_state'] = json_encode($l10nState);
 
         $queryBuilder = $connectionPool->getQueryBuilderForTable($tableName);
         foreach ($updateValues as $updateFieldName => $updateValue) {
@@ -233,18 +222,14 @@ class L10nModeUpdater implements RowUpdaterInterface
         foreach ($tableDefinition['columns'] as $fieldName => $fieldConfiguration) {
             if (
                 empty($fieldConfiguration['l10n_mode'])
-                && !empty($this->migratedL10nCoreFieldNames[$tableName][$fieldName])
-            ) {
-                $fieldConfiguration['l10n_mode'] = $this->migratedL10nCoreFieldNames[$tableName][$fieldName];
-            }
-
-            if (
-                empty($fieldConfiguration['l10n_mode'])
                 || empty($fieldConfiguration['config']['type'])
             ) {
                 continue;
             }
-            if ($fieldConfiguration['l10n_mode'] === 'mergeIfNotBlank') {
+            if (
+                $fieldConfiguration['l10n_mode'] === 'exclude'
+                || $fieldConfiguration['l10n_mode'] === 'mergeIfNotBlank'
+            ) {
                 $fields[$fieldName] = $fieldConfiguration;
             }
         }
@@ -253,35 +238,17 @@ class L10nModeUpdater implements RowUpdaterInterface
             return $payload;
         }
 
-        $parentQueryBuilder = $connectionPool->getQueryBuilderForTable($tableName);
-        $parentQueryBuilder->getRestrictions()->removeAll();
-        $parentQueryBuilder->from($tableName);
+        $queryBuilder = $connectionPool->getQueryBuilderForTable($tableName);
+        $queryBuilder->getRestrictions()->removeAll();
+        $queryBuilder->from($tableName);
 
-        $predicates = [];
         foreach ($fields as $fieldName => $fieldConfiguration) {
-            $predicates[] = $parentQueryBuilder->expr()->comparison(
-                $parentQueryBuilder->expr()->trim($fieldName),
-                ExpressionBuilder::EQ,
-                $parentQueryBuilder->createNamedParameter('', \PDO::PARAM_STR)
-            );
-            $predicates[] = $parentQueryBuilder->expr()->eq(
-                $fieldName,
-                $parentQueryBuilder->createNamedParameter('', \PDO::PARAM_STR)
-            );
-
             if (empty($fieldConfiguration['config']['type'])) {
                 continue;
             }
 
             if ($fieldConfiguration['config']['type'] === 'group') {
                 $fieldTypes[$fieldName] = 'group';
-                $predicates[] = $parentQueryBuilder->expr()->isNull(
-                    $fieldName
-                );
-                $predicates[] = $parentQueryBuilder->expr()->eq(
-                    $fieldName,
-                    $parentQueryBuilder->createNamedParameter('0', \PDO::PARAM_STR)
-                );
             }
             if (
                 $fieldConfiguration['config']['type'] === 'inline'
@@ -291,32 +258,6 @@ class L10nModeUpdater implements RowUpdaterInterface
                 && $fieldConfiguration['config']['foreign_table'] === 'sys_file_reference'
             ) {
                 $fieldTypes[$fieldName] = 'inline/FAL';
-
-                $childQueryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_reference');
-                $childQueryBuilder->getRestrictions()->removeAll();
-                $childExpression = $childQueryBuilder
-                    ->count('uid')
-                    ->from('sys_file_reference')
-                    ->andWhere(
-                        $childQueryBuilder->expr()->eq(
-                            'sys_file_reference.uid_foreign',
-                            $parentQueryBuilder->getConnection()->quoteIdentifier($tableName . '.uid')
-                        ),
-                        $childQueryBuilder->expr()->eq(
-                            'sys_file_reference.tablenames',
-                            $parentQueryBuilder->createNamedParameter($tableName, \PDO::PARAM_STR)
-                        ),
-                        $childQueryBuilder->expr()->eq(
-                            'sys_file_reference.fieldname',
-                            $parentQueryBuilder->createNamedParameter($fieldName, \PDO::PARAM_STR)
-                        )
-                    );
-
-                $predicates[] = $parentQueryBuilder->expr()->comparison(
-                    '(' . $childExpression->getSQL() . ')',
-                    ExpressionBuilder::GT,
-                    $parentQueryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
-                );
             }
         }
 
@@ -330,18 +271,17 @@ class L10nModeUpdater implements RowUpdaterInterface
             );
         }
 
-        $statement = $parentQueryBuilder
+        $statement = $queryBuilder
             ->select(...$selectFieldNames)
             ->andWhere(
-                $parentQueryBuilder->expr()->gt(
+                $queryBuilder->expr()->gt(
                     $tableDefinition['ctrl']['languageField'],
-                    $parentQueryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
+                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
                 ),
-                $parentQueryBuilder->expr()->gt(
+                $queryBuilder->expr()->gt(
                     $sourceFieldName,
-                    $parentQueryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
-                ),
-                $parentQueryBuilder->expr()->orX(...$predicates)
+                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
+                )
             )
             ->execute();
 
index 02d04ae..d4a6b3e 100644 (file)
                        <trans-unit id="imwizard.crop-height">
                                <source>height:</source>
                        </trans-unit>
+                       <trans-unit id="localizationStateSelector.header">
+                           <source>Translation behavior</source>
+                       </trans-unit>
+                       <trans-unit id="localizationStateSelector.customValue">
+                           <source>Custom value</source>
+                       </trans-unit>
+                       <trans-unit id="localizationStateSelector.defaultLanguageValue">
+                           <source>Value of default language</source>
+                       </trans-unit>
+                       <trans-unit id="localizationStateSelector.sourceLanguageValue">
+                           <source>Value of %1s language</source>
+                       </trans-unit>
                </body>
        </file>
 </xliff>