diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index f3ad5fb32b..57756eb653 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -5,7 +5,6 @@ * Provides hook implementations for Layout Builder. */ -use Drupal\Component\Plugin\ConfigurableInterface; use Drupal\Core\Breadcrumb\Breadcrumb; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Entity\EntityInterface; @@ -19,7 +18,6 @@ use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplayStorage; use Drupal\layout_builder\Form\BlockContentInlineBlockTranslateForm; -use Drupal\layout_builder\Form\BlockPluginTranslationForm; use Drupal\layout_builder\Form\DefaultsEntityForm; use Drupal\layout_builder\Form\LayoutBuilderEntityViewDisplayForm; use Drupal\layout_builder\Form\OverridesEntityForm; @@ -412,19 +410,3 @@ function layout_builder_preprocess_language_content_settings_table(&$variables) } } } - -/** - * Implements hook_block_alter(). - * - * Ensures every block plugin definition has an 'layout_builder_translation' - * form specified. - */ -function layout_builder_block_alter(&$definitions) { - foreach ($definitions as &$definition) { - // If a block plugin does not define its own 'layout_builder_translation' - // form use the one provided by this module. - if (!isset($definition['forms']['layout_builder_translation']) && !empty($definition['class']) && in_array(ConfigurableInterface::class, class_implements($definition['class']))) { - $definition['forms']['layout_builder_translation'] = BlockPluginTranslationForm::class; - } - } -} diff --git a/core/modules/layout_builder/src/Element/LayoutBuilder.php b/core/modules/layout_builder/src/Element/LayoutBuilder.php index 62bd6fce85..9d6018443e 100644 --- a/core/modules/layout_builder/src/Element/LayoutBuilder.php +++ b/core/modules/layout_builder/src/Element/LayoutBuilder.php @@ -166,21 +166,6 @@ protected function layout(SectionStorageInterface $section_storage) { // Mark this UI as uncacheable. $output['#cache']['max-age'] = 0; - if ($is_translation) { - $has_translatable_component = FALSE; - foreach ($section_storage->getSections() as $section) { - foreach ($section->getComponents() as $uuid => $component) { - if ($component->hasTranslatableConfiguration()) { - $has_translatable_component = TRUE; - break 2; - } - } - } - if (!$has_translatable_component) { - $this->messenger()->addStatus($this->t('There are currently no settings that can be translated')); - } - } - return $output; } @@ -469,23 +454,21 @@ protected function createContextualLinkElement(SectionStorageInterface $section_ ]; if (static::isTranslation($section_storage)) { $component = $section->getComponent($uuid); - if ($component->hasTranslatableConfiguration()) { - $contextual_group = 'layout_builder_block_translation'; - /** @var \Drupal\Core\Language\LanguageInterface $language */ - if ($language = $section_storage->getTranslationLanguage()) { - $contextual_link_settings['route_parameters']['langcode'] = $language->getId(); - } + $contextual_group = 'layout_builder_block_translation'; + /** @var \Drupal\Core\Language\LanguageInterface $language */ + if ($language = $section_storage->getTranslationLanguage()) { + $contextual_link_settings['route_parameters']['langcode'] = $language->getId(); + } - /** @var \Drupal\layout_builder\Plugin\Block\InlineBlock $plugin */ - $plugin = $component->getPlugin(); - if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'inline_block') { - $configuration = $plugin->getConfiguration(); - /** @var \Drupal\block_content\Entity\BlockContent $block */ - $block = $this->entityTypeManager->getStorage('block_content') - ->loadRevision($configuration['block_revision_id']); - if ($block->isTranslatable()) { - $contextual_group = 'layout_builder_inline_block_translation'; - } + /** @var \Drupal\layout_builder\Plugin\Block\InlineBlock $plugin */ + $plugin = $component->getPlugin(); + if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'inline_block') { + $configuration = $plugin->getConfiguration(); + /** @var \Drupal\block_content\Entity\BlockContent $block */ + $block = $this->entityTypeManager->getStorage('block_content') + ->loadRevision($configuration['block_revision_id']); + if ($block->isTranslatable()) { + $contextual_group = 'layout_builder_inline_block_translation'; } } } diff --git a/core/modules/layout_builder/src/Form/BlockPluginTranslationForm.php b/core/modules/layout_builder/src/Form/BlockPluginTranslationForm.php deleted file mode 100644 index 9224bcaf8c..0000000000 --- a/core/modules/layout_builder/src/Form/BlockPluginTranslationForm.php +++ /dev/null @@ -1,69 +0,0 @@ -plugin->getConfiguration(); - $form['label'] = [ - '#title' => $this->t('Label'), - '#type' => 'textfield', - '#default_value' => isset($this->translatedConfiguration['label']) ? $this->translatedConfiguration['label'] : $configuration['label'], - '#required' => TRUE, - ]; - return $form; - } - - /** - * {@inheritdoc} - */ - public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { - } - - /** - * {@inheritdoc} - * - * We only saving the label translation the label the form values will be - * saved in \Drupal\layout_builder\Form\TranslateBlockForm::submitForm(). - */ - public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { - } - - /** - * {@inheritdoc} - */ - public function setTranslatedConfiguration(array $translated_configuration) { - $this->translatedConfiguration = $translated_configuration; - } - -} diff --git a/core/modules/layout_builder/src/Form/OverridesEntityForm.php b/core/modules/layout_builder/src/Form/OverridesEntityForm.php index 731ae19432..12c36d5ca8 100644 --- a/core/modules/layout_builder/src/Form/OverridesEntityForm.php +++ b/core/modules/layout_builder/src/Form/OverridesEntityForm.php @@ -92,6 +92,7 @@ protected function init(FormStateInterface $form_state) { 'weight' => -10, 'settings' => [], ]); + $this->setFormDisplay($form_display, $form_state); } diff --git a/core/modules/layout_builder/src/Form/TranslateBlockForm.php b/core/modules/layout_builder/src/Form/TranslateBlockForm.php index 3c051f1bb4..f7e3df6fcc 100644 --- a/core/modules/layout_builder/src/Form/TranslateBlockForm.php +++ b/core/modules/layout_builder/src/Form/TranslateBlockForm.php @@ -2,13 +2,19 @@ namespace Drupal\layout_builder\Form; -use Drupal\Core\Block\BlockPluginInterface; +use Drupal\Component\Utility\Html; +use Drupal\config_translation\Form\ConfigTranslationFormBase; +use Drupal\Core\Ajax\AjaxFormHelperTrait; +use Drupal\Core\Config\TypedConfigManagerInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Form\SubformState; -use Drupal\Core\Plugin\PluginWithFormsInterface; -use Drupal\layout_builder\LayoutBuilderPluginTranslationFormInterface; -use Drupal\layout_builder\SectionStorageInterface; +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\TypedData\TraversableTypedDataInterface; +use Drupal\layout_builder\Controller\LayoutRebuildTrait; +use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; use Drupal\layout_builder\TranslatableSectionStorageInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a form to translate a block plugin in the Layout Builder. @@ -16,13 +22,62 @@ * @internal * Form classes are internal. */ -class TranslateBlockForm extends ConfigureBlockFormBase { +class TranslateBlockForm extends FormBase { + + use AjaxFormHelperTrait; + use LayoutRebuildTrait; + + /** + * The section storage. + * + * @var \Drupal\layout_builder\TranslatableSectionStorageInterface + */ + protected $sectionStorage; + + /** + * The UUID of the component. + * + * @var string + */ + protected $uuid; + + /** + * The layout tempstore repository. + * + * @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface + */ + protected $layoutTempstoreRepository; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * @var \Drupal\Core\Config\TypedConfigManagerInterface + */ + protected $typedConfigManager; + + /** + * Constructs a new TranslateBlockForm. + */ + public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, ModuleHandlerInterface $module_handler, TypedConfigManagerInterface $typed_config_manager) { + $this->layoutTempstoreRepository = $layout_tempstore_repository; + $this->moduleHandler = $module_handler; + $this->typedConfigManager = $typed_config_manager; + } /** * {@inheritdoc} */ - protected function submitLabel() { - return $this->t('Translate'); + public static function create(ContainerInterface $container) { + return new static( + $container->get('layout_builder.tempstore_repository'), + $container->get('module_handler'), + $container->get('config.typed') + ); } /** @@ -33,7 +88,7 @@ public function getFormId() { } /** - * Builds the block form. + * Builds the block translation form. * * @param array $form * An associative array containing the structure of the form. @@ -51,40 +106,113 @@ public function getFormId() { * @return array * The form array. */ - public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $uuid = NULL) { + public function buildForm(array $form, FormStateInterface $form_state, TranslatableSectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $uuid = NULL) { $component = $section_storage->getSection($delta)->getComponent($uuid); - return $this->doBuildForm($form, $form_state, $section_storage, $delta, $component); + + $this->sectionStorage = $section_storage; + $this->uuid = $component->getUuid(); + + $configuration = $component->getPlugin()->getConfiguration(); + $type_definition = $this->typedConfigManager->getDefinition('block.settings.' . $component->getPlugin()->getPluginId()); + /** @var \Drupal\Core\TypedData\DataDefinitionInterface $definition */ + $definition = new $type_definition['definition_class']($type_definition); + $definition->setClass($type_definition['class']); + + /** @var \Drupal\Core\Config\Schema\Mapping $typed_data */ + $typed_data = $type_definition['class']::createInstance($definition); + $typed_data->setValue($configuration); + $translated_config = $this->sectionStorage->getTranslatedComponentConfiguration($this->uuid); + foreach (array_keys($configuration) as $key) { + if (!isset($translated_config[$key])) { + $translated_config[$key] = NULL; + } + } + + $form['translation'] = $this->createTranslationElement($section_storage->getSourceLanguage(), $section_storage->getTranslationLanguage(), $typed_data, $translated_config); + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Translate'), + ]; + + if ($this->isAjax()) { + $form['submit']['#ajax']['callback'] = '::ajaxSubmit'; + // @todo static::ajaxSubmit() requires data-drupal-selector to be the same + // between the various Ajax requests. A bug in + // \Drupal\Core\Form\FormBuilder prevents that from happening unless + // $form['#id'] is also the same. Normally, #id is set to a unique HTML + // ID via Html::getUniqueId(), but here we bypass that in order to work + // around the data-drupal-selector bug. This is okay so long as we + // assume that this form only ever occurs once on a page. Remove this + // workaround in https://www.drupal.org/node/2897377. + $form['#id'] = Html::getId($form_state->getBuildInfo()['form_id']); + } + return $form; } /** - * {@inheritdoc} + * Creates translation element. + * + * @param \Drupal\Core\Language\LanguageInterface $source_language + * The source language. + * @param \Drupal\Core\Language\LanguageInterface $translation_language + * The translation language. + * @param \Drupal\Core\TypedData\TraversableTypedDataInterface $typed_data + * The typed data of the configuration settings. + * @param array $translated_configuration + * The translated configuration. + * + * @return array + * The translation element render array. */ - protected function getPluginForm(BlockPluginInterface $block) { - if ($block instanceof PluginWithFormsInterface && $this->sectionStorage instanceof TranslatableSectionStorageInterface) { - $plugin_form = $this->pluginFormFactory->createInstance($block, 'layout_builder_translation'); - if ($plugin_form instanceof LayoutBuilderPluginTranslationFormInterface) { - $plugin_form->setTranslatedConfiguration($this->sectionStorage->getTranslatedComponentConfiguration($this->uuid)); + protected function createTranslationElement(LanguageInterface $source_language, LanguageInterface $translation_language, TraversableTypedDataInterface $typed_data, array $translated_configuration) { + if ($this->moduleHandler->moduleExists('config_translation')) { + // If config_translation is installed let it handle creating complex + // schema. + $form_element = ConfigTranslationFormBase::createFormElement($typed_data); + $element_build = $form_element->getTranslationBuild($source_language, $translation_language, $typed_data->getValue(), $translated_configuration, []); + } + else { + /** @var \Drupal\Core\TypedData\TypedDataInterface $typed_datum */ + foreach ($typed_data as $key => $typed_datum) { + $definition = $typed_datum->getDataDefinition(); + $data_type = $definition->getDataType(); + + // Provide translation of top level label and text items. + if ($data_type === 'label' || $data_type === 'text') { + $element_build[$key]['source'] = [ + '#type' => 'item', + '#title' => $this->t($definition->getLabel()), + '#markup' => $typed_datum->getValue()?: '(' . $this->t('Empty') . ')', + '#parents' => ['source', $key], + ]; + $element_build[$key]['translation'] = [ + '#type' => $data_type === 'label' ? 'textfield' : 'textarea', + '#title' => $this->t($definition->getLabel()), + '#default_value' => isset($translated_configuration[$key]) ? $translated_configuration[$key] : '', + '#parents' => ['translation', $key], + ]; + } } - return $plugin_form; } - return $block; + $element_build['#tree'] = TRUE; + return $element_build; } /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - // Call the plugin submit handler. - $subform_state = SubformState::createForSubform($form['settings'], $form, $form_state); - $this->getPluginForm($this->block)->submitConfigurationForm($form, $subform_state); - - $settings = $subform_state->getValues(); - /** @var \Drupal\layout_builder\TranslatableSectionStorageInterface $section_storage */ $section_storage = $this->sectionStorage; - $section_storage->setTranslatedComponentConfiguration($this->uuid, $settings); - + $section_storage->setTranslatedComponentConfiguration($this->uuid, $form_state->getValue('translation')); $this->layoutTempstoreRepository->set($this->sectionStorage); $form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl()); } + /** + * {@inheritdoc} + */ + protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) { + return $this->rebuildAndClose($this->sectionStorage); + } + } diff --git a/core/modules/layout_builder/src/LayoutBuilderPluginTranslationFormInterface.php b/core/modules/layout_builder/src/LayoutBuilderPluginTranslationFormInterface.php deleted file mode 100644 index ba7424c5fe..0000000000 --- a/core/modules/layout_builder/src/LayoutBuilderPluginTranslationFormInterface.php +++ /dev/null @@ -1,18 +0,0 @@ -getEntity(); - return $block_content->isTranslatable() || (!empty($this->configuration['label_display']) && !empty($this->configuration['label'])); - } - } diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php index 7be41a55f3..0d5f3f91d7 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php @@ -519,4 +519,16 @@ public function getTranslationLanguage() { return NULL; } + /** + * {@inheritdoc} + */ + public function getSourceLanguage() { + if (!$this->isDefaultTranslation()) { + /** @var \Drupal\Core\Entity\TranslatableInterface $entity */ + $entity = $this->getEntity(); + return $entity->getUntranslated()->language(); + } + return NULL; + } + } diff --git a/core/modules/layout_builder/src/SectionComponent.php b/core/modules/layout_builder/src/SectionComponent.php index 5d9250ca31..4003f7e7e7 100644 --- a/core/modules/layout_builder/src/SectionComponent.php +++ b/core/modules/layout_builder/src/SectionComponent.php @@ -2,7 +2,6 @@ namespace Drupal\layout_builder; -use Drupal\Component\Plugin\ConfigurableInterface; use Drupal\Component\Plugin\Exception\PluginException; use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent; @@ -318,24 +317,4 @@ public static function fromArray(array $component) { ))->setWeight($component['weight']); } - /** - * Determines whether the component has translatable configuration. - * - * @return bool - * TRUE if the plugin has translatable configuration. - */ - public function hasTranslatableConfiguration() { - $plugin = $this->getPlugin(); - if ($plugin instanceof LayoutBuilderTranslatablePluginInterface) { - return $plugin->hasTranslatableConfiguration(); - } - elseif ($plugin instanceof ConfigurableInterface) { - // For all plugins that do not implement - // LayoutBuilderTranslatablePluginInterface only allow label translation. - $configuration = $plugin->getConfiguration(); - return !empty($configuration['label_display']) && !empty($configuration['label']); - } - return FALSE; - } - } diff --git a/core/modules/layout_builder/src/TranslatableSectionStorageInterface.php b/core/modules/layout_builder/src/TranslatableSectionStorageInterface.php index 25047f4d7d..88aa16dfa1 100644 --- a/core/modules/layout_builder/src/TranslatableSectionStorageInterface.php +++ b/core/modules/layout_builder/src/TranslatableSectionStorageInterface.php @@ -53,4 +53,13 @@ public function getTranslatedConfiguration(); */ public function getTranslationLanguage(); + /** + * Gets the source language of the translation if any. + * + * @return \Drupal\Core\Language\LanguageInterface|null + * The translation source language if the current layout is for a + * translation otherwise NULL. + */ + public function getSourceLanguage(); + } diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTranslationTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTranslationTest.php index 2717534c76..64a2986513 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTranslationTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTranslationTest.php @@ -93,6 +93,7 @@ public function testInlineBlockContentTranslation() { static::INLINE_BLOCK_LOCATOR, 'Block en label', 'Block it label', + '', ['[name="settings[block_form][body][0][value]"]'] ); diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/JavascriptTranslationTestTrait.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/JavascriptTranslationTestTrait.php index 0b2e054e7f..6cf00e9e17 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/JavascriptTranslationTestTrait.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/JavascriptTranslationTestTrait.php @@ -19,14 +19,15 @@ * @param array $unexpected_element_selectors * A list of selectors for elements that should be present. */ - protected function updateBlockTranslation($block_selector, $expected_label, $new_label, array $unexpected_element_selectors = []) { + protected function updateBlockTranslation($block_selector, $untranslated_label, $new_label, $expected_label = '', array $unexpected_element_selectors = []) { /** @var \Drupal\Tests\WebAssert $assert_session */ $assert_session = $this->assertSession(); $page = $this->getSession()->getPage(); $this->clickContextualLink($block_selector, 'Translate block'); - $label_input = $assert_session->waitForElementVisible('css', '#drupal-off-canvas [name="settings[label]"]'); + $label_input = $assert_session->waitForElementVisible('css', '#drupal-off-canvas [name="translation[label]"]'); $this->assertNotEmpty($label_input); $this->assertEquals($expected_label, $label_input->getValue()); + $assert_session->elementTextContains('css', '#drupal-off-canvas .form-item-source-label', $untranslated_label); $label_input->setValue($new_label); foreach ($unexpected_element_selectors as $unexpected_element_selector) { $assert_session->elementNotExists('css', $unexpected_element_selector); diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/TranslationTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/TranslationTest.php index 23b7046f5a..541d9f1b5e 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/TranslationTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/TranslationTest.php @@ -130,37 +130,26 @@ public function testLabelTranslation() { $this->drupalGet('it/node/1/layout'); $this->assertNonTranslationActionsRemoved(); $this->updateBlockTranslation('.block-system-powered-by-block', 'untranslated label', 'label in translation'); - // @todo this will fail until https://www.drupal.org/node/3039185 - // $this->updateBlockTranslation('.block-field-blocknodebundle-with-section-fieldbody', 'field label untranslated', 'field label translated'); $assert_session->buttonExists('Save layout'); $page->pressButton('Save layout'); $assert_session->addressEquals('it/node/1'); $assert_session->pageTextContains('label in translation'); $assert_session->pageTextNotContains('untranslated label'); - // @todo this will fail until https://www.drupal.org/node/3039185 - // $assert_session->pageTextContains('field label translated'); // Confirm that untranslated label is still used on default translation. $this->drupalGet('node/1'); $assert_session->pageTextContains('untranslated label'); $assert_session->pageTextNotContains('label in translation'); - // @todo this will fail until https://www.drupal.org/node/3039185 - // $assert_session->pageTextContains('field label translated'); - // $assert_session->pageTextNotContains('field label untranslated'); // Update the translations block label. $this->drupalGet('it/node/1/layout'); $this->assertNonTranslationActionsRemoved(); - $this->updateBlockTranslation('.block-system-powered-by-block', 'label in translation', 'label updated in translation'); - // @todo this will fail until https://www.drupal.org/node/3039185 - // $this->updateBlockTranslation('.block-field-blocknodebundle-with-section-fieldbody', 'field label untranslated', 'field label translated'); + $this->updateBlockTranslation('.block-system-powered-by-block', 'untranslated label', 'label updated in translation', 'label in translation'); $assert_session->buttonExists('Save layout'); $page->pressButton('Save layout'); $assert_session->addressEquals('it/node/1'); $assert_session->pageTextContains('label updated in translation'); $assert_session->pageTextNotContains('label in translation'); - // @todo this will fail until https://www.drupal.org/node/3039185 - // $assert_session->pageTextContains('field label translated'); } }