Problem/Motivation

In #3043651: Update the Layout field to non-translatable when possible. and #3041659: Remove the layout tab from translations because Layout Builder does not support translations yet Layout Builder was made to not support translations, because the existing functionality was not complete and broken in some ways. This is a plan/meta issue to flesh out a complete solution to Layout Builder translatability.

Translated layouts could mean 2 different things and could happen on 2 different levels

Layout Translation Types

  1. Synced-Layout Translations: In this case the actual layouts, the placements of blocks with in sections, would be the same across all languages. The strings of the block labels and the inline blocks(if translatable) would be able to be translated per language.
  2. Independent Layout Translations: In this case the layouts for each translation would be totally independent. They could have different blocks with different settings and have different sections. There would be no synchronization across translations. This scenario probably more of localization than translation

Translation Levels

  1. Bundle level defaults: This would be for Layout Defaults configured for each bundle, on the manage display. This could include different layouts for each view mode.
  2. Layout Overrides: These were be per entity translated layouts

Proposed resolution

Support Synced-Layout Translations on both the Bundle level defaults and Overrides level.
Child Issues
#2946333: Allow synced Layout override Translations: translating labels and inline blocks
#3044993: Allow synced Layout default Translations: translating labels and inline blocks
Both of these issue should allow translating strings and inline blocks in UI that shows the current layout.

Related: #2916876: Add visibility control conditions to blocks within Layout Builder
would allow certain blocks to only appear on certain languages.

Independent Layout Translations: will not currently be support by core.

Remaining tasks

All of them.

User interface changes

Layouts will be translatable if configured like so.

API changes

TBD.

Data model changes

TBD.

Release notes snippet

TBD.

Comments

Gábor Hojtsy created an issue. See original summary.

johnwebdev’s picture

From all the discussions in Slack and #2946333: Allow synced Layout override Translations: translating labels and inline blocks, from my perspective there are at least these use cases

  • To be able to translate the configuration and contents of a Layout (labels, inline blocks etc)
  • To be able to have separate layouts per translation

Has been referred as "Symmetric vs Asymmetric" translations in the past.

And the first use case seems to be the most common requirement, however, looking at the contrib. ecosystem i.e. #2461695: Support asymmetric translations the second use case is also something that is wanted.

If for some reason, we cannot support the second use case in core, at least we should make sure its possible to do in a contrib. module.


Blocks and inline blocks placed in a layout are only stored as references in Layout Builder, similar to how Entity Reference field works.

If a Entity Reference field is translatable, you may have separate referenced items per translation and if it's not translatable, the same references are shown, but rendered in the language of the viewed parent.

IMO, this behaviour should apply to Layout Builder.

johnwebdev’s picture

tedbow’s picture

Issue summary: View changes

Update Problem section

tedbow’s picture

From the summary I added and from the feedback we got while doing #2946333: Allow synced Layout override Translations: translating labels and inline blocks I think the minimum we should try to support would be Synced-Layout Translations on both the defaults and overrides. Also included in that minimum would be making sure we don't create a system that is not flexible enough to allow contrib to support Independent Layout Translations.

I think we should also attempt to support Independent Layout Translations on the override level but we would have to very mindful of the UX this cause if we try support both types on overrides.

tim.plunkett’s picture

Issue tags: +Blocks-Layouts
Berdir’s picture

As mentioned earlier, this is what we call symmetric and asymmetric translations in paragraphs and we currently only support symetric in the main module, but there's a contrib module and issues/patches for the other variant.

I don't know the layout builder structure in detail, but the advantage of paragraphs/ERR is that the field itself doesn't store any translatable things and is very simple/structured, just two integer references. So we can just make the field untranslatable, handle the logic of displaying/editing the right translation of the paragraphs in the widget and we're done (as long as you ignore workflows, see below). (Obviously even that part is far from simple, because current entity forms are not very good at tracking the current language, it can be changed and so on..)

However, it's more complicated for you, because even when you also just store references (non-reusable content blocks), then those are in a blob, and you can also have block settings that can be translatable labels, like the block title. Given that, I'm not sure that an untranslatable field is even an option for you. It sounds more like image field translations work, where each property can be translated or not, and the untranslatable ones are synced between all translations. Except in your case it's much more complicated, as it would be based on the block config schema to decide recursively what can be merged/changed in a translation and what can't? Such a feature could also be interesting for the block_field module, as well as things like commerce shipping methods, which has a visible shipping rate label in within plugin settings stored in a field (#2934142: Shipping method entity does not support translations).

Where it gets really complicated is workflows and specifically drafts, because each translation is basically its own revision branch *but* each revisions contains all translations that exist at that point in time. Imagine this scenario:

1. You create a published EN node with a non-reusable content block, node revision 1 (N1) and block revision 1 (B1).
2. Now you create a EN draft and also change the block giving you N2 and B2.
3. Now you want to translate it and add a DE draft and also translate the block as N3 and B3.
4. Then you publish the EN draft and maybe you also make a few more changes to that, keeping it published, that gives you N4 + N5 and B4 and B5 (maybe the block has fewer revisions, maybe not)
5. Finally, you also want to to publish the DE draft, which is N3 and B3. But you can't just take those revisions, because they do not contain the more recent EN changes. So what core does at this point is prepare N6 based on the current default revision N5 (so there could even be extra EN drafts now), then copies over all the DE values of translatable fields into that and saves that.

Now, if you don't do anything, if the layout field is translatable, then you go back to the EN content in B3 because the whole thing is copied over 1:1. And if it's not translatable, you don't actually get the DE translation of the block. Either way, you lose. Additionally, entities also need to know which translations changed in a revision, which it for example uses to filter the list of revisions shown based on the current language. And if you think that's not enough fun yet, add a few more translations to the mix.

What we did for paragraphs is we put several things into core that are explained in this change record: https://www.drupal.org/node/2975280. This allows us to...

a) show the widget of our untranslatable field on translations (we disallow all structure changes ourself then, like add/remove/reorder paragraphs). This is not directly an issue for you, but you still need to figure out how the UI should look/work while being on a translation.
b) then we basically lie to core about the field having language-affecting changes (technically it always changes on translations, but we'll deal with that later)
c) finally, we use the revision create hook to repeat the merge process that core just did for the host entity and repeat that (recursively) for all referenced paragraphs.

As far as I understand, layout builder *must* implement something similar, except again, I expect it to be more complicated, as you need to merge whole block configurations, which themself might contain references which need to be merged too.

That's where we are now with paragraphs and it works quite well, but there is at least one major limitation. It is currently not possible to prepare new paragraphs (or reorder/remove) in a draft EN translation and then add translations based on that draft, because when creating a draft for DE, it will be based on the default revision, not the EN draft. layout builder should have the same problem, just replace "add paragraph" with "change layout/place block". I should have tried to find solutions for that for months now, but I'm kinda stuck. See #3004099: Allow to translate paragraphs from pending revisions for the paragraphs issue for that and #3007233: Draft translations should be based on the latest revision of the source language, not the published version for the core issue that I opened as a possible option for resolving that.

tedbow’s picture

We discussed Layout Builder and Translations at Drupalcon Seattle.

Attended by: @xjm, framework manager, @Gábor Hojtsy, product manager, @plach, Content Translation maintainer, @tedbow(me) Layout Builder maintainer, @DyanneNova Layout Builder maintainer, @Kristen Pol, and @EclipseGc.

It was decided that core would support Synced-Layout Translations(as described in Issue Summary here) on both the Defaults and Overrides level.

So initially we not support Independent Layout Translations for either defaults or overrides. We would try in #2946333: Allow synced Layout override Translations: translating labels and inline blocks to make it possible for contrib to provide Independent Layout Translations in overrides.

If contrib provide this and it seemed to be a good fit for core we would consider this after 8.8.x but currently the plan is not to support it. Supporting Independent Layout Translations at the default level was not considered not likely for core inclusion.

To discuss the details of the currently 2 planned levels of translations please use the existing issues: #2946333: Allow synced Layout override Translations: translating labels and inline blocks & #3044993: Allow synced Layout default Translations: translating labels and inline blocks

I will quote/copy the @Berdir's comment in #7 over to #2946333: Allow synced Layout override Translations: translating labels and inline blocks

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

xmacinfo’s picture

Just found out about this issue. Using Drupal 8.9.3.

I programmatically created a block and assigned that block in a layout.

Result: the block is not translatable and is displayed in English on the French side. This is a broken behavior.

What are the solutions to display a custom block in the proper language when displayed inside a layout?

J-Lee’s picture

@xmacinfo I do this by creating a custom block in the block layout section (/block/add). Then the block is translatable. Later I place the block over the layout builder. To help the authors, I use the patch from #3020876: Contextual links of reusable content blocks are not displayed when rendering entities built via Layout Builder to add contextual links.
Because of this problem, we use the layout builder only for a few specific pages.

But you want to do it programmatically ...
Can you add translations to the block entity (block_content) before you add them to the layout?
Something like $block->addTranslation(...)

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

xmacinfo’s picture

@J-Lee Thanks. I was able to achieve what I was looking for with /admin/structure/block/block-content/types/add and using the patch from:

https://www.drupal.org/project/drupal/issues/3020876

ichionid’s picture

Hey people,
We really need this feature in our e-com platform. I was thinking if there is a way to help on this one. It's very difficult to convince e-com that they should create a new page instead of a translation every time.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

gabrielraymer’s picture

For our use case as IRS.gov, independent translations are a critical feature. We'd greatly appreciate consideration for independent translations to be part of core. At this time contrib does provide a solution for asymmetric (independent) translations.
https://www.drupal.org/project/layout_builder_at
The asymmetric translations module has a similar number of downloads as the symmetric translation module this seems to indicate that interest in both feature is equally needed across the community.

seanB’s picture

I added the following to a custom module to enable asymmetric translations. Hope it helps.

/**
 * Implements hook_entity_translation_create().
 */
function mymodule_entity_translation_create(EntityInterface $translation) {
  if ($translation instanceof NodeInterface) {
    \Drupal::service('class_resolver')
      ->getInstanceFromDefinition(TranslationCreate::class)
      ->create($translation);
  }
}

With the following class to duplicate all blocks:

<?php

namespace Drupal\mymodule;

use Drupal\layout_builder\Plugin\Block\InlineBlock;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\SectionComponent;
use Drupal\node\NodeInterface;

/**
 * Helper class for content translations.
 */
class TranslationCreate {

  /**
   * Creates a translation for a node.
   *
   * @param \Drupal\node\NodeInterface $translation
   *   The node to create a translation for.
   */
  public function create(NodeInterface $translation): void {
    // When an entity is translated, make sure we duplicate all its blocks in
    // layout builder. We don't want multiple translations of the same entity
    // managing the same block.
    if (!$translation->hasField(OverridesSectionStorage::FIELD_NAME)) {
      return;
    }

    // Get the default translation to copy blocks from.
    $default_translation = $translation->getUntranslated();

    // Create duplicates of the blocks and sections in the correct translation.
    /** @var \Drupal\layout_builder\SectionListInterface $layout_field */
    $layout_field = clone $default_translation->get(OverridesSectionStorage::FIELD_NAME);
    foreach ($layout_field->getSections() as $sid => $section) {
      // Create a duplicate of each component.
      foreach ($section->getComponents() as $component) {
        $block = $component->getPlugin();

        // Only clone inline blocks.
        if (!$block instanceof InlineBlock) {
          continue;
        }

        $component_array = $component->toArray();
        $configuration = $component_array['configuration'];

        // Fetch the block content.
        $block_content = NULL;
        if (!empty($configuration['block_serialized'])) {
          $block_content = unserialize($configuration['block_serialized']);
        }
        elseif (!empty($configuration['block_revision_id'])) {
          $block_content = \Drupal::entityTypeManager()->getStorage('block_content')
            ->loadRevision($configuration['block_revision_id']);
        }

        // Create a duplicate block.
        if ($block_content) {
          /** @var \Drupal\block_content\BlockContentInterface $block_content */
          $cloned_block_content = $block_content->createDuplicate();

          // Set the langcode to the translation's langcode.
          $cloned_block_content->set('langcode', $translation->language()->getId());

          // Unset the revision and add the serialized block content.
          $configuration['block_revision_id'] = NULL;
          $configuration['block_serialized'] = serialize($cloned_block_content);
        }

        // Wrap the block in a section component.
        $new_component = new SectionComponent(
          \Drupal::service('uuid')->generate(),
          $component_array['region'],
          $configuration,
          $component_array['additional']
        );

        // Remove existing components from the section and append a fresh copy.
        $section->insertAfterComponent($component->getUuid(), $new_component);
        $section->removeComponent($component->getUuid());
      }

      $layout_field->insertSection($sid, $section);
      $layout_field->removeSection($sid + 1);
    }

    // Set the updated layout in the translation.
    $translation->set(OverridesSectionStorage::FIELD_NAME, $layout_field->getValue());
  }

}

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.