Commit 2fd70c83 authored by Oliver Hader's avatar Oliver Hader Committed by Christian Kuhn
Browse files

[FEATURE] Introduce allowLanguageSynchronization

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: default avatarTYPO3com <no-reply@typo3.com>
Reviewed-by: Frans Saris's avatarFrans Saris <franssaris@gmail.com>
Tested-by: Frans Saris's avatarFrans Saris <franssaris@gmail.com>
Reviewed-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
parent 1dd78c74
......@@ -61,6 +61,15 @@ class InlineControlContainer extends AbstractContainer
*/
protected $requireJsModules = [];
/**
* @var array Default wizards
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
];
/**
* Container objects give $nodeFactory down to other containers.
*
......@@ -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;
......
......@@ -27,8 +27,14 @@ class CheckboxElement extends AbstractFormElement
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
......
......@@ -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',
......
......@@ -34,8 +34,14 @@ class ImageManipulationElement extends AbstractFormElement
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
......
......@@ -30,8 +30,14 @@ class InputColorPickerElement extends AbstractFormElement
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
......
......@@ -31,8 +31,14 @@ class InputDateTimeElement extends AbstractFormElement
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
......
......@@ -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',
],
],
];
......
......@@ -33,8 +33,14 @@ class InputTextElement extends AbstractFormElement
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
......
......@@ -25,8 +25,14 @@ class RadioElement extends AbstractFormElement
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
......
......@@ -33,8 +33,14 @@ class SelectCheckBoxElement extends AbstractFormElement
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
......
......@@ -55,8 +55,14 @@ class SelectMultipleSideBySideElement extends AbstractFormElement
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
......
......@@ -42,8 +42,14 @@ class SelectSingleBoxElement extends AbstractFormElement
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
......
......@@ -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',
......
......@@ -31,8 +31,14 @@ class TextElement extends AbstractFormElement
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
......
......@@ -30,8 +30,14 @@ class TextTableElement extends AbstractFormElement
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector'
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
......
<?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'];
}
}
......@@ -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.
......
......@@ -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
);
}
}
}
}
......
......@@ -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;
}
}
......@@ -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,
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment