From c32049af02447e849285dcaee337e418b2ade81f Mon Sep 17 00:00:00 2001
From: Oliver Hader <oliver@typo3.org>
Date: Tue, 13 Jun 2017 17:40:05 +0200
Subject: [PATCH] [BUGFIX] Allow processing of multiple new record
 localizations

The current implementation of DataMapProcessor to determine the necessity
of synchronizing record localizations is too strict since it expects real
persisted database records and cannot resolve new records that have been
handed with the very same data-map to the DataHandler.

This mentioned constraint is resolved. Besides that, an additional value
processing for internal fields is added. This method resolves new record
ids to their real persisted UIDs using the famous remap stack. This only
is executed, if these fields have not been processed in a relation-aware
context and have a meaning to TYPO3 data-structures - for instance this
is the case for 'l10n_source' field defined as TCA type 'passthrough'.

Change-Id: Id1291910d85b3d314af7203314b8696a337fe364
Resolves: #80239
Releases: master, 8.7
Reviewed-on: https://review.typo3.org/52871
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
---
 .../core/Classes/DataHandling/DataHandler.php | 54 +++++++++-
 .../Localization/DataMapProcessor.php         | 99 +++++++++++++------
 .../DataHandling/Localization/State.php       | 38 +++----
 .../ForeignField/AbstractActionTestCase.php   | 55 +++++++++++
 .../IRRE/ForeignField/Modify/ActionTest.php   | 30 ++++++
 ...NestedChildrenWLanguageSynchronization.csv | 35 +++++++
 ...izeParentContentSelectWSynchronization.csv | 29 ++++++
 .../Regular/AbstractActionTestCase.php        | 29 ++++++
 .../Regular/Modify/ActionTest.php             | 45 +++++++++
 .../Modify/DataSet/createLocalizedContent.csv | 11 +++
 .../createLocalizedContentWExclude.csv        | 11 +++
 ...createLocalizedContentWSynchronization.csv | 11 +++
 12 files changed, 397 insertions(+), 50 deletions(-)
 create mode 100644 typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentNCreateNestedChildrenWLanguageSynchronization.csv
 create mode 100644 typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentSelectWSynchronization.csv
 create mode 100644 typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/createLocalizedContent.csv
 create mode 100644 typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/createLocalizedContentWExclude.csv
 create mode 100644 typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/createLocalizedContentWSynchronization.csv

diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
index 966331595d7a..6c93c540b29b 100644
--- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php
+++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
@@ -1660,6 +1660,56 @@ class DataHandler
             default:
                 // Do nothing
         }
+        $res = $this->checkValueForInternalReferences($res, $value, $tcaFieldConf, $table, $id, $field);
+        return $res;
+    }
+
+    /**
+     * Checks values that are used for internal references. If the provided $value
+     * is a NEW-identifier, the direct processing is stopped. Instead, the value is
+     * forwarded to the remap-stack to be post-processed and resolved into a proper
+     * UID after all data has been resolved.
+     *
+     * This method considers TCA types that cannot handle and resolve these internal
+     * values directly, like 'passthrough', 'none' or 'user'. Values are only modified
+     * here if the $field is used as 'transOrigPointerField' or 'translationSource'.
+     *
+     * @param array $res The result array. The processed value (if any!) is set in the 'value' key.
+     * @param string $value The value to set.
+     * @param array $tcaFieldConf Field configuration from TCA
+     * @param string $table Table name
+     * @param int $id UID of record
+     * @param string $field The field name
+     * @return array The result array. The processed value (if any!) is set in the "value" key.
+     */
+    protected function checkValueForInternalReferences(array $res, $value, $tcaFieldConf, $table, $id, $field)
+    {
+        $relevantFieldNames = [
+            $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null,
+            $GLOBALS['TCA'][$table]['ctrl']['translationSource'] ?? null,
+        ];
+
+        if (
+            // in case the field is not relevant
+            !in_array($field, $relevantFieldNames)
+            // in case the 'value' index has been unset already
+            || !array_key_exists('value', $res)
+            // in case it's not a NEW-identifier
+            || strpos($value, 'NEW') === false
+        ) {
+            return $res;
+        }
+
+        $valueArray = [$value];
+        $this->remapStackRecords[$table][$id] = ['remapStackIndex' => count($this->remapStack)];
+        $this->addNewValuesToRemapStackChildIds($valueArray);
+        $this->remapStack[] = [
+            'args' => [$valueArray, $tcaFieldConf, $id, $table, $field],
+            'pos' => ['valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 3],
+            'field' => $field
+        ];
+        unset($res['value']);
+
         return $res;
     }
 
@@ -6127,7 +6177,9 @@ class DataHandler
                     $remapAction['args'][$remapAction['pos']['valueArray']] = $valueArray;
                 }
                 // Process the arguments with the defined function:
-                $newValue = call_user_func_array([$this, $remapAction['func']], $remapAction['args']);
+                if (!empty($remapAction['func'])) {
+                    $newValue = call_user_func_array([$this, $remapAction['func']], $remapAction['args']);
+                }
                 // If array is returned, check for maxitems condition, if string is returned this was already done:
                 if (is_array($newValue)) {
                     $newValue = implode(',', $this->checkValue_checkMax($tcaFieldConf, $newValue));
diff --git a/typo3/sysext/core/Classes/DataHandling/Localization/DataMapProcessor.php b/typo3/sysext/core/Classes/DataHandling/Localization/DataMapProcessor.php
index de0e7cfcfec1..6161d61e8cb9 100644
--- a/typo3/sysext/core/Classes/DataHandling/Localization/DataMapProcessor.php
+++ b/typo3/sysext/core/Classes/DataHandling/Localization/DataMapProcessor.php
@@ -175,10 +175,7 @@ class DataMapProcessor
 
         $dependencies = $this->fetchDependencies(
             $forTableName,
-            $this->filterNewItemIds(
-                $forTableName,
-                $this->filterNumericIds(array_keys($idValues))
-            )
+            $this->filterNewItemIds($forTableName, $idValues)
         );
 
         foreach ($idValues as $id => $values) {
@@ -287,19 +284,25 @@ class DataMapProcessor
      *
      * @param DataMapItem $item
      * @param array $fieldNames
-     * @param int $fromId
+     * @param string|int $fromId
      */
-    protected function synchronizeTranslationItem(DataMapItem $item, array $fieldNames, int $fromId)
+    protected function synchronizeTranslationItem(DataMapItem $item, array $fieldNames, $fromId)
     {
         if (empty($fieldNames)) {
             return;
         }
+
         $fieldNameList = 'uid,' . implode(',', $fieldNames);
-        $fromRecord = BackendUtility::getRecordWSOL(
-            $item->getFromTableName(),
-            $fromId,
-            $fieldNameList
-        );
+
+        $fromRecord = ['uid' => $fromId];
+        if (MathUtility::canBeInterpretedAsInteger($fromId)) {
+            $fromRecord = BackendUtility::getRecordWSOL(
+                $item->getFromTableName(),
+                $fromId,
+                $fieldNameList
+            );
+        }
+
         $forRecord = [];
         if (!$item->isNew()) {
             $forRecord = BackendUtility::getRecordWSOL(
@@ -308,6 +311,7 @@ class DataMapProcessor
                 $fieldNameList
             );
         }
+
         foreach ($fieldNames as $fieldName) {
             $this->synchronizeFieldValues(
                 $item,
@@ -326,10 +330,6 @@ class DataMapProcessor
      */
     protected function populateTranslationItem(DataMapItem $item)
     {
-        if ($item->isNew()) {
-            return;
-        }
-
         foreach ([DataMapItem::SCOPE_PARENT, DataMapItem::SCOPE_SOURCE] as $scope) {
             foreach ($item->findDependencies($scope) as $dependentItem) {
                 // use suggested item, if it was submitted in data-map
@@ -389,10 +389,15 @@ class DataMapProcessor
         }
 
         $fromId = $fromRecord['uid'];
+        // retrieve value from in-memory data-map
         if ($this->isSetInDataMap($item->getFromTableName(), $fromId, $fieldName)) {
             $fromValue = $this->allDataMap[$item->getFromTableName()][$fromId][$fieldName];
-        } else {
+        // retrieve value from record
+        } elseif (array_key_exists($fieldName, $fromRecord)) {
             $fromValue = $fromRecord[$fieldName];
+        // otherwise abort synchronization
+        } else {
+            return;
         }
 
         // plain values
@@ -569,9 +574,9 @@ class DataMapProcessor
             return;
         }
         // In case only missing elements shall be created, re-use previously sanitized
-        // values IF child table cannot be translated, the relation parent item is new
-        // and the count of missing relations equals the count of previously sanitized
-        // relations. This is caused during copy processes, when the child relations
+        // values IF the relation parent item is new and the count of missing relations
+        // equals the count of previously sanitized relations.
+        // This is caused during copy processes, when the child relations
         // already have been cloned in DataHandler::copyRecord_procBasedOnFieldType()
         // without the possibility to resolve the initial connections at this point.
         // Otherwise child relations would superfluously be duplicated again here.
@@ -579,7 +584,7 @@ class DataMapProcessor
         $sanitizedValue = $this->sanitizationMap[$item->getTableName()][$item->getId()][$fieldName] ?? null;
         if (
             !empty($missingAncestorIds) && $item->isNew() && $sanitizedValue !== null
-            && count(GeneralUtility::trimExplode(',', $sanitizedValue)) === count($missingAncestorIds)
+            && count(GeneralUtility::trimExplode(',', $sanitizedValue, true)) === count($missingAncestorIds)
         ) {
             $this->modifyDataMap(
                 $item->getTableName(),
@@ -634,16 +639,17 @@ class DataMapProcessor
             foreach ($populateAncestorIds as $populateAncestorId) {
                 $newLocalizationId = StringUtility::getUniqueId('NEW');
                 $desiredIdMap[$populateAncestorId] = $newLocalizationId;
+                $duplicatedValues = $this->duplicateFromDataMap(
+                    $foreignTableName,
+                    $populateAncestorId,
+                    $item->getLanguage(),
+                    $fieldNames,
+                    !$isLocalizationModeExclude && $isTranslatable
+                );
                 $this->modifyDataMap(
                     $foreignTableName,
                     $newLocalizationId,
-                    $this->duplicateFromDataMap(
-                        $foreignTableName,
-                        $populateAncestorId,
-                        $item->getLanguage(),
-                        $fieldNames,
-                        !$isLocalizationModeExclude && $isTranslatable
-                    )
+                    $duplicatedValues
                 );
             }
         }
@@ -779,7 +785,7 @@ class DataMapProcessor
      * + [7]   -> []                               # since there's nothing
      *
      * @param string $tableName
-     * @param array $ids
+     * @param int[]|string[] $ids
      * @return DataMapItem[][]
      */
     protected function fetchDependencies(string $tableName, array $ids)
@@ -801,8 +807,22 @@ class DataMapProcessor
         if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['translationSource'])) {
             $fieldNames['source'] = $GLOBALS['TCA'][$tableName]['ctrl']['translationSource'];
         }
+        $fieldNamesMap = array_combine($fieldNames, $fieldNames);
+
+        $persistedIds = $this->filterNumericIds(array_keys($ids), true);
+        $createdIds = $this->filterNumericIds(array_keys($ids), false);
+        $dependentElements = $this->fetchDependentElements($tableName, $persistedIds, $fieldNames);
 
-        $dependentElements = $this->fetchDependentElements($tableName, $ids, $fieldNames);
+        foreach ($createdIds as $createdId) {
+            $data = $this->allDataMap[$tableName][$createdId] ?? null;
+            if ($data === null) {
+                continue;
+            }
+            $dependentElements[] = array_merge(
+                ['uid' => $createdId],
+                array_intersect_key($data, $fieldNamesMap)
+            );
+        }
 
         $dependencyMap = [];
         foreach ($dependentElements as $dependentElement) {
@@ -1026,7 +1046,7 @@ class DataMapProcessor
      *
      * @param string[]|int[] $ids
      * @param bool $numeric
-     * @return array
+     * @return int[]|string[]
      */
     protected function filterNumericIds(array $ids, bool $numeric = true)
     {
@@ -1153,6 +1173,10 @@ class DataMapProcessor
             // @todo Not sure, whether $id is resolved in DataHandler's remapStack
             $data[$fieldNames['source']] = $fromId;
         }
+        // unset field names that are expected to be handled in this processor
+        foreach ($this->getFieldNamesToBeHandled($tableName) as $fieldName) {
+            unset($data[$fieldName]);
+        }
 
         $prefixFieldNames = array_intersect(
             array_keys($data),
@@ -1241,6 +1265,21 @@ class DataMapProcessor
         return $localizationExcludeFieldNames;
     }
 
+    /**
+     * Gets a list of field names which have to be handled. Basically this
+     * includes fields using allowLanguageSynchronization or l10n_mode=exclude.
+     *
+     * @param string $tableName
+     * @return string[]
+     */
+    protected function getFieldNamesToBeHandled(string $tableName)
+    {
+        return array_merge(
+            State::getFieldNames($tableName),
+            $this->getLocalizationModeExcludeFieldNames($tableName)
+        );
+    }
+
     /**
      * Field names of TCA table with columns having l10n_mode=prefixLangTitle
      *
diff --git a/typo3/sysext/core/Classes/DataHandling/Localization/State.php b/typo3/sysext/core/Classes/DataHandling/Localization/State.php
index 8c8f437494b3..9c1026d00b30 100644
--- a/typo3/sysext/core/Classes/DataHandling/Localization/State.php
+++ b/typo3/sysext/core/Classes/DataHandling/Localization/State.php
@@ -74,6 +74,25 @@ class State
         ;
     }
 
+    /**
+     * @param string $tableName
+     * @return array
+     */
+    public static function getFieldNames(string $tableName)
+    {
+        return array_keys(
+            array_filter(
+                $GLOBALS['TCA'][$tableName]['columns'],
+                function (array $fieldConfiguration) {
+                    return !empty(
+                        $fieldConfiguration['config']
+                            ['behaviour']['allowLanguageSynchronization']
+                    );
+                }
+            )
+        );
+    }
+
     /**
      * @param string $tableName
      * @return bool
@@ -104,25 +123,6 @@ class State
         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
      */
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/AbstractActionTestCase.php b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/AbstractActionTestCase.php
index 4a07afeef703..c196738ab6fe 100644
--- a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/AbstractActionTestCase.php
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/AbstractActionTestCase.php
@@ -14,6 +14,8 @@ namespace TYPO3\CMS\Core\Tests\Functional\DataHandling\IRRE\ForeignField;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Utility\StringUtility;
+
 /**
  * Functional test for the DataHandler
  */
@@ -35,10 +37,12 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D
     const TABLE_Content = 'tt_content';
     const TABLE_Hotel = 'tx_irretutorial_1nff_hotel';
     const TABLE_Offer = 'tx_irretutorial_1nff_offer';
+    const TABLE_Price = 'tx_irretutorial_1nff_price';
 
     const FIELD_PageHotel = 'tx_irretutorial_hotels';
     const FIELD_ContentHotel = 'tx_irretutorial_1nff_hotels';
     const FIELD_HotelOffer = 'offers';
+    const FIELD_OfferPrice = 'prices';
 
     /**
      * @var string
@@ -192,6 +196,22 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D
         $this->recordIds['localizedContentId'] = $newTableIds[self::TABLE_Content][self::VALUE_ContentIdLast];
     }
 
+    /**
+     * @see DataSet/localizeParentContentSelectWSynchronization.csv
+     */
+    public function localizeParentContentInSelectModeWithLanguageSynchronization()
+    {
+        $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['localizationMode'] = 'select';
+        $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['localizeChildrenAtParentLocalization'] = false;
+        $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['allowLanguageSynchronization'] = true;
+        $GLOBALS['TCA'][self::TABLE_Hotel]['columns'][self::FIELD_HotelOffer]['config']['behaviour']['localizeChildrenAtParentLocalization'] = false;
+        $GLOBALS['TCA'][self::TABLE_Hotel]['columns'][self::FIELD_HotelOffer]['config']['behaviour']['allowLanguageSynchronization'] = true;
+        $GLOBALS['TCA'][self::TABLE_Offer]['columns'][self::FIELD_OfferPrice]['config']['behaviour']['localizeChildrenAtParentLocalization'] = false;
+        $GLOBALS['TCA'][self::TABLE_Offer]['columns'][self::FIELD_OfferPrice]['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];
+    }
+
     /**
      * @see DataSet/localizeParentContentWAllChildrenSelect.csv
      */
@@ -251,6 +271,41 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D
         );
     }
 
+    /**
+     * @see DataSet/Modify/localizeParentContentNCreateNestedChildrenWLanguageSynchronization.csv
+     */
+    public function localizeParentContentAndCreateNestedChildrenWithLanguageSynchronization()
+    {
+        $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['allowLanguageSynchronization'] = true;
+        $GLOBALS['TCA'][self::TABLE_Hotel]['columns'][self::FIELD_HotelOffer]['config']['behaviour']['allowLanguageSynchronization'] = true;
+        $GLOBALS['TCA'][self::TABLE_Offer]['columns'][self::FIELD_OfferPrice]['config']['behaviour']['allowLanguageSynchronization'] = true;
+
+        $newTableIds = $this->actionService->localizeRecord(self::TABLE_Content, self::VALUE_ContentIdLast, self::VALUE_LanguageId);
+        $this->recordIds['localizedContentIdFirst'] = $newTableIds[self::TABLE_Content][self::VALUE_ContentIdLast];
+
+        $newHotelId = StringUtility::getUniqueId('NEW');
+        $newOfferId = StringUtility::getUniqueId('NEW');
+        $newPriceId = StringUtility::getUniqueId('NEW');
+        $dataMap = [
+            self::TABLE_Content => [
+                self::VALUE_ContentIdLast => [self::FIELD_ContentHotel => '5,' . $newHotelId],
+            ],
+            self::TABLE_Hotel => [
+                $newHotelId => ['pid' => self::VALUE_PageId, 'title' => 'New Hotel #1', 'offers' => $newOfferId],
+            ],
+            self::TABLE_Offer => [
+                $newOfferId => ['pid' => self::VALUE_PageId, 'title' => 'New Offer #1.1', 'prices' => $newPriceId],
+            ],
+            self::TABLE_Price => [
+                $newPriceId => ['pid' => self::VALUE_PageId, 'title' => 'New Price #1.1.1'],
+            ],
+        ];
+        $this->actionService->invoke($dataMap, []);
+        $this->recordIds['newHoteId'] = $this->actionService->getDataHandler()->substNEWwithIDs[$newHotelId];
+        $this->recordIds['newOfferId'] = $this->actionService->getDataHandler()->substNEWwithIDs[$newOfferId];
+        $this->recordIds['newPriceId'] = $this->actionService->getDataHandler()->substNEWwithIDs[$newPriceId];
+    }
+
     /**
      * @see DataSet/changeParentContentRecordSorting.csv
      */
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/ActionTest.php b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/ActionTest.php
index df12b51e5e59..4ea180631563 100644
--- a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/ActionTest.php
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/ActionTest.php
@@ -213,6 +213,21 @@ class ActionTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\IRRE\Fore
             ->setTable(self::TABLE_Hotel)->setField('title')->setValues('[Translate to Dansk:] Hotel #1'));
     }
 
+    /**
+     * @test
+     * @see DataSet/localizeParentContentSelectWSynchronization.csv
+     */
+    public function localizeParentContentInSelectModeWithLanguageSynchronization()
+    {
+        parent::localizeParentContentInSelectModeWithLanguageSynchronization();
+        $this->assertAssertionDataSet('localizeParentContentSelectWSynchronization');
+
+        $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)
+            ->setTable(self::TABLE_Hotel)->setField('title')->setValues('[Translate to Dansk:] Hotel #1'));
+    }
+
     /**
      * @test
      * @see DataSet/localizeParentContentWAllChildrenSelect.csv
@@ -258,6 +273,21 @@ class ActionTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\IRRE\Fore
             ->setTable(self::TABLE_Hotel)->setField('title')->setValues('[Translate to Deutsch:] [Translate to Dansk:] Hotel #1', '[Translate to Deutsch:] [Translate to Dansk:] Hotel #2'));
     }
 
+    /**
+     * @test
+     * @see DataSet/Modify/localizeParentContentNCreateNestedChildrenWLanguageSynchronization.csv
+     */
+    public function localizeParentContentAndCreateNestedChildrenWithLanguageSynchronization()
+    {
+        parent::localizeParentContentAndCreateNestedChildrenWithLanguageSynchronization();
+        $this->assertAssertionDataSet('localizeParentContentNCreateNestedChildrenWLanguageSynchronization');
+
+        $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)
+            ->setTable(self::TABLE_Hotel)->setField('title')->setValues('[Translate to Dansk:] Hotel #1', '[Translate to Dansk:] New Hotel #1'));
+    }
+
     /**
      * @test
      * @see DataSet/changeParentContentRecordSorting.csv
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentNCreateNestedChildrenWLanguageSynchronization.csv b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentNCreateNestedChildrenWLanguageSynchronization.csv
new file mode 100644
index 000000000000..1f078ca60408
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentNCreateNestedChildrenWLanguageSynchronization.csv
@@ -0,0 +1,35 @@
+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,l10n_state
+,297,89,256,0,0,0,0,0,0,0,0,0,"Regular Element #1",2,\NULL
+,298,89,512,0,0,0,0,0,0,0,0,0,"Regular Element #2",2,\NULL
+,299,89,768,0,1,298,298,0,0,0,0,0,"[Translate to Dansk:] Regular Element #2",2,"{""tx_irretutorial_1nff_hotels"":""parent""}"
+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,l10n_state
+,2,89,1024,0,0,0,0,0,0,0,0,0,"Hotel #0",89,pages,,0,\NULL
+,3,89,1280,0,0,0,0,0,0,0,0,0,"Hotel #1",297,tt_content,,2,\NULL
+,4,89,1792,0,0,0,0,0,0,0,0,0,"Hotel #2",297,tt_content,,1,\NULL
+,5,89,1,0,0,0,0,0,0,0,0,0,"Hotel #1",298,tt_content,,1,\NULL
+,6,89,1,0,1,5,5,0,0,0,0,0,"[Translate to Dansk:] Hotel #1",299,tt_content,,1,"{""offers"":""parent""}"
+,7,89,2,0,0,0,0,0,0,0,0,0,"New Hotel #1",298,tt_content,,1,\NULL
+,8,89,2,0,1,7,0,0,0,0,0,0,"[Translate to Dansk:] New Hotel #1",299,tt_content,,1,"{""offers"":""parent""}"
+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,l10n_state
+,5,89,1024,0,0,0,0,0,0,0,0,0,"Offer #1.1",3,tx_irretutorial_1nff_hotel,,3,\NULL
+,6,89,1792,0,0,0,0,0,0,0,0,0,"Offer #1.2",3,tx_irretutorial_1nff_hotel,,2,\NULL
+,7,89,1280,0,0,0,0,0,0,0,0,0,"Offer #2.1",4,tx_irretutorial_1nff_hotel,,1,\NULL
+,8,89,1536,0,0,0,0,0,0,0,0,0,"Offer #1.1",5,tx_irretutorial_1nff_hotel,,1,\NULL
+,9,89,768,0,1,8,8,0,0,0,0,0,"[Translate to Dansk:] Offer #1.1",6,tx_irretutorial_1nff_hotel,,1,"{""prices"":""parent""}"
+,10,89,1,0,0,0,0,0,0,0,0,0,"New Offer #1.1",7,tx_irretutorial_1nff_hotel,,1,\NULL
+,11,89,1,0,1,10,0,0,0,0,0,0,"[Translate to Dansk:] New Offer #1.1",8,tx_irretutorial_1nff_hotel,,1,"{""prices"":""parent""}"
+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,l10n_state
+,7,89,1024,0,0,0,0,0,0,0,0,0,"Price #1.1.1",5,tx_irretutorial_1nff_offer,,\NULL
+,8,89,2048,0,0,0,0,0,0,0,0,0,"Price #1.1.2",5,tx_irretutorial_1nff_offer,,\NULL
+,9,89,2560,0,0,0,0,0,0,0,0,0,"Price #1.1.3",5,tx_irretutorial_1nff_offer,,\NULL
+,10,89,1280,0,0,0,0,0,0,0,0,0,"Price #1.2.1",6,tx_irretutorial_1nff_offer,,\NULL
+,11,89,2304,0,0,0,0,0,0,0,0,0,"Price #1.2.2",6,tx_irretutorial_1nff_offer,,\NULL
+,12,89,1536,0,0,0,0,0,0,0,0,0,"Price #2.1.1",7,tx_irretutorial_1nff_offer,,\NULL
+,13,89,1792,0,0,0,0,0,0,0,0,0,"Price #1.1.1",8,tx_irretutorial_1nff_offer,,\NULL
+,14,89,768,0,1,13,13,0,0,0,0,0,"[Translate to Dansk:] Price #1.1.1",9,tx_irretutorial_1nff_offer,,\NULL
+,15,89,1,0,0,0,0,0,0,0,0,0,"New Price #1.1.1",10,tx_irretutorial_1nff_offer,,\NULL
+,16,89,1,0,1,15,0,0,0,0,0,0,"[Translate to Dansk:] New Price #1.1.1",11,tx_irretutorial_1nff_offer,,\NULL
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentSelectWSynchronization.csv b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentSelectWSynchronization.csv
new file mode 100644
index 000000000000..22262d2dd2f0
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentSelectWSynchronization.csv
@@ -0,0 +1,29 @@
+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,l10n_state
+,297,89,256,0,0,0,0,0,0,0,0,0,"Regular Element #1",2,\NULL
+,298,89,512,0,0,0,0,0,0,0,0,0,"Regular Element #2",1,\NULL
+,299,89,768,0,1,298,298,0,0,0,0,0,"[Translate to Dansk:] Regular Element #2",1,"{""tx_irretutorial_1nff_hotels"":""parent""}"
+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,l10n_state
+,2,89,512,0,0,0,0,0,0,0,0,0,"Hotel #0",89,pages,,0,\NULL
+,3,89,768,0,0,0,0,0,0,0,0,0,"Hotel #1",297,tt_content,,2,\NULL
+,4,89,1536,0,0,0,0,0,0,0,0,0,"Hotel #2",297,tt_content,,1,\NULL
+,5,89,1024,0,0,0,0,0,0,0,0,0,"Hotel #1",298,tt_content,,1,\NULL
+,6,89,1,0,1,5,5,0,0,0,0,0,"[Translate to Dansk:] Hotel #1",299,tt_content,,1,"{""offers"":""parent""}"
+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,l10n_state
+,5,89,512,0,0,0,0,0,0,0,0,0,"Offer #1.1",3,tx_irretutorial_1nff_hotel,,3,\NULL
+,6,89,1536,0,0,0,0,0,0,0,0,0,"Offer #1.2",3,tx_irretutorial_1nff_hotel,,2,\NULL
+,7,89,768,0,0,0,0,0,0,0,0,0,"Offer #2.1",4,tx_irretutorial_1nff_hotel,,1,\NULL
+,8,89,1024,0,0,0,0,0,0,0,0,0,"Offer #1.1",5,tx_irretutorial_1nff_hotel,,1,\NULL
+,9,89,1,0,1,8,8,0,0,0,0,0,"[Translate to Dansk:] Offer #1.1",6,tx_irretutorial_1nff_hotel,,1,"{""prices"":""parent""}"
+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,l10n_state
+,7,89,512,0,0,0,0,0,0,0,0,0,"Price #1.1.1",5,tx_irretutorial_1nff_offer,,\NULL
+,8,89,1792,0,0,0,0,0,0,0,0,0,"Price #1.1.2",5,tx_irretutorial_1nff_offer,,\NULL
+,9,89,2304,0,0,0,0,0,0,0,0,0,"Price #1.1.3",5,tx_irretutorial_1nff_offer,,\NULL
+,10,89,768,0,0,0,0,0,0,0,0,0,"Price #1.2.1",6,tx_irretutorial_1nff_offer,,\NULL
+,11,89,2048,0,0,0,0,0,0,0,0,0,"Price #1.2.2",6,tx_irretutorial_1nff_offer,,\NULL
+,12,89,1024,0,0,0,0,0,0,0,0,0,"Price #2.1.1",7,tx_irretutorial_1nff_offer,,\NULL
+,13,89,1280,0,0,0,0,0,0,0,0,0,"Price #1.1.1",8,tx_irretutorial_1nff_offer,,\NULL
+,14,89,1,0,1,13,13,0,0,0,0,0,"[Translate to Dansk:] Price #1.1.1",9,tx_irretutorial_1nff_offer,,\NULL
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Regular/AbstractActionTestCase.php b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/AbstractActionTestCase.php
index 50c249673793..cdabe46de66e 100644
--- a/typo3/sysext/core/Tests/Functional/DataHandling/Regular/AbstractActionTestCase.php
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/AbstractActionTestCase.php
@@ -14,6 +14,8 @@ namespace TYPO3\CMS\Core\Tests\Functional\DataHandling\Regular;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Utility\StringUtility;
+
 /**
  * Functional test for the DataHandler
  */
@@ -189,6 +191,33 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D
         $this->actionService->modifyRecord(self::TABLE_Content, self::VALUE_ContentIdThird, ['header' => 'Testing #1']);
     }
 
+    public function createLocalizedContent()
+    {
+        $newContentIdDefault = StringUtility::getUniqueId('NEW');
+        $newContentIdLocalized = StringUtility::getUniqueId('NEW');
+        $dataMap = [
+            self::TABLE_Content => [
+                $newContentIdDefault => ['pid' => self::VALUE_PageId, 'header' => 'Testing'],
+                $newContentIdLocalized => ['pid' => self::VALUE_PageId, 'header' => 'Localized Testing', 'sys_language_uid' => self::VALUE_LanguageId, 'l18n_parent' => $newContentIdDefault, 'l10n_source' => $newContentIdDefault],
+            ]
+        ];
+        $this->actionService->invoke($dataMap, []);
+        $this->recordIds['newContentIdDefault'] = $this->actionService->getDataHandler()->substNEWwithIDs[$newContentIdDefault];
+        $this->recordIds['newContentIdLocalized'] = $this->actionService->getDataHandler()->substNEWwithIDs[$newContentIdLocalized];
+    }
+
+    public function createLocalizedContentWithLanguageSynchronization()
+    {
+        $GLOBALS['TCA']['tt_content']['columns']['header']['config']['behaviour']['allowLanguageSynchronization'] = true;
+        self::createLocalizedContent();
+    }
+
+    public function createLocalizedContentWithLocalizationExclude()
+    {
+        $GLOBALS['TCA']['tt_content']['columns']['header']['l10n_mode'] = 'exclude';
+        self::createLocalizedContent();
+    }
+
     /**
      * @see DataSet/changeContentRecordSorting.csv
      */
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/ActionTest.php b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/ActionTest.php
index 1afcd9a7d143..32cd9b13b4c9 100644
--- a/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/ActionTest.php
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/ActionTest.php
@@ -252,6 +252,51 @@ class ActionTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\Regular\A
             ->setTable(self::TABLE_Content)->setField('header')->setValues('[Translate to Deutsch:] [Translate to Dansk:] Regular Element #1', 'Testing #1'));
     }
 
+    /**
+     * @test
+     * @see DataSet/createLocalizedContent.csv
+     */
+    public function createLocalizedContent()
+    {
+        parent::createLocalizedContent();
+
+        $this->assertAssertionDataSet('createLocalizedContent');
+
+        $responseSections = $this->getFrontendResponse(self::VALUE_PageId, self::VALUE_LanguageId)->getResponseSections();
+        $this->assertThat($responseSections, $this->getRequestSectionHasRecordConstraint()
+            ->setTable(self::TABLE_Content)->setField('header')->setValues('Localized Testing'));
+    }
+
+    /**
+     * @test
+     * @see DataSet/createLocalizedContentWSynchronization.csv
+     */
+    public function createLocalizedContentWithLanguageSynchronization()
+    {
+        parent::createLocalizedContentWithLanguageSynchronization();
+
+        $this->assertAssertionDataSet('createLocalizedContentWSynchronization');
+
+        $responseSections = $this->getFrontendResponse(self::VALUE_PageId, self::VALUE_LanguageId)->getResponseSections();
+        $this->assertThat($responseSections, $this->getRequestSectionHasRecordConstraint()
+            ->setTable(self::TABLE_Content)->setField('header')->setValues('Testing'));
+    }
+
+    /**
+     * @test
+     * @see DataSet/createLocalizedContentWExclude.csv
+     */
+    public function createLocalizedContentWithLocalizationExclude()
+    {
+        parent::createLocalizedContentWithLocalizationExclude();
+
+        $this->assertAssertionDataSet('createLocalizedContentWExclude');
+
+        $responseSections = $this->getFrontendResponse(self::VALUE_PageId, self::VALUE_LanguageId)->getResponseSections();
+        $this->assertThat($responseSections, $this->getRequestSectionHasRecordConstraint()
+            ->setTable(self::TABLE_Content)->setField('header')->setValues('Testing'));
+    }
+
     /**
      * @test
      * @see DataSet/changeContentRecordSorting.csv
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/createLocalizedContent.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/createLocalizedContent.csv
new file mode 100644
index 000000000000..08a16a3e0f47
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/createLocalizedContent.csv
@@ -0,0 +1,11 @@
+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,l10n_state
+,296,88,256,0,0,0,0,0,0,0,0,0,0,"Regular Element #0",
+,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,"Regular Element #2",
+,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,128,0,0,0,0,0,0,0,0,0,0,Testing,\NULL
+,304,89,64,0,1,303,303,0,0,0,0,0,0,"Localized Testing",\NULL
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/createLocalizedContentWExclude.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/createLocalizedContentWExclude.csv
new file mode 100644
index 000000000000..aaa64fe17015
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/createLocalizedContentWExclude.csv
@@ -0,0 +1,11 @@
+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,l10n_state
+,296,88,256,0,0,0,0,0,0,0,0,0,0,"Regular Element #0",
+,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,"Regular Element #2",
+,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,128,0,0,0,0,0,0,0,0,0,0,Testing,\NULL
+,304,89,64,0,1,303,303,0,0,0,0,0,0,Testing,\NULL
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/createLocalizedContentWSynchronization.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/createLocalizedContentWSynchronization.csv
new file mode 100644
index 000000000000..40cb1d661ca9
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/createLocalizedContentWSynchronization.csv
@@ -0,0 +1,11 @@
+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,l10n_state
+,296,88,256,0,0,0,0,0,0,0,0,0,0,"Regular Element #0",
+,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,"Regular Element #2",
+,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,128,0,0,0,0,0,0,0,0,0,0,Testing,\NULL
+,304,89,64,0,1,303,303,0,0,0,0,0,0,Testing,"{""header"":""parent""}"
-- 
GitLab