Initial Drupal 11 with DDEV setup
This commit is contained in:
		@ -0,0 +1,6 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies: {  }
 | 
			
		||||
id: test
 | 
			
		||||
label: null
 | 
			
		||||
description: null
 | 
			
		||||
@ -0,0 +1,17 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  config:
 | 
			
		||||
    - entity_test.entity_test_bundle.test
 | 
			
		||||
  module:
 | 
			
		||||
    - content_translation
 | 
			
		||||
third_party_settings:
 | 
			
		||||
  content_translation:
 | 
			
		||||
    enabled: true
 | 
			
		||||
    bundle_settings:
 | 
			
		||||
      untranslatable_fields_hide: '0'
 | 
			
		||||
id: entity_test_with_bundle.test
 | 
			
		||||
target_entity_type_id: entity_test_with_bundle
 | 
			
		||||
target_bundle: test
 | 
			
		||||
default_langcode: site_default
 | 
			
		||||
language_alterable: true
 | 
			
		||||
@ -0,0 +1,9 @@
 | 
			
		||||
name: 'Content translation tests'
 | 
			
		||||
type: module
 | 
			
		||||
description: 'Provides content translation tests.'
 | 
			
		||||
package: Testing
 | 
			
		||||
version: VERSION
 | 
			
		||||
dependencies:
 | 
			
		||||
  - drupal:content_translation
 | 
			
		||||
  - drupal:language
 | 
			
		||||
  - drupal:entity_test
 | 
			
		||||
@ -0,0 +1,42 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\content_translation_test\Entity;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\Attribute\ContentEntityType;
 | 
			
		||||
use Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider;
 | 
			
		||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTest;
 | 
			
		||||
use Drupal\entity_test\EntityTestForm;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Defines the test entity class.
 | 
			
		||||
 */
 | 
			
		||||
#[ContentEntityType(
 | 
			
		||||
  id: 'entity_test_translatable_no_skip',
 | 
			
		||||
  label: new TranslatableMarkup('Test entity - Translatable check UI'),
 | 
			
		||||
  entity_keys: [
 | 
			
		||||
    'id' => 'id',
 | 
			
		||||
    'uuid' => 'uuid',
 | 
			
		||||
    'bundle' => 'type',
 | 
			
		||||
    'label' => 'name',
 | 
			
		||||
    'langcode' => 'langcode',
 | 
			
		||||
  ],
 | 
			
		||||
  handlers: [
 | 
			
		||||
    'form' => ['default' => EntityTestForm::class],
 | 
			
		||||
    'route_provider' => [
 | 
			
		||||
      'html' => DefaultHtmlRouteProvider::class,
 | 
			
		||||
    ],
 | 
			
		||||
  ],
 | 
			
		||||
  links: [
 | 
			
		||||
    'edit-form' => '/entity_test_translatable_no_skip/{entity_test_translatable_no_skip}/edit',
 | 
			
		||||
  ],
 | 
			
		||||
  admin_permission: 'administer entity_test content',
 | 
			
		||||
  base_table: 'entity_test_mul',
 | 
			
		||||
  data_table: 'entity_test_mul_property_data',
 | 
			
		||||
  translatable: TRUE,
 | 
			
		||||
)]
 | 
			
		||||
class EntityTestTranslatableNoUISkip extends EntityTest {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,33 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\content_translation_test\Entity;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\Attribute\ContentEntityType;
 | 
			
		||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTest;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Defines the test entity class.
 | 
			
		||||
 */
 | 
			
		||||
#[ContentEntityType(
 | 
			
		||||
  id: 'entity_test_translatable_UI_skip',
 | 
			
		||||
  label: new TranslatableMarkup('Test entity - Translatable skip UI check'),
 | 
			
		||||
  entity_keys: [
 | 
			
		||||
    'id' => 'id',
 | 
			
		||||
    'uuid' => 'uuid',
 | 
			
		||||
    'bundle' => 'type',
 | 
			
		||||
    'label' => 'name',
 | 
			
		||||
    'langcode' => 'langcode',
 | 
			
		||||
  ],
 | 
			
		||||
  base_table: 'entity_test_mul',
 | 
			
		||||
  data_table: 'entity_test_mul_property_data',
 | 
			
		||||
  translatable: TRUE,
 | 
			
		||||
  additional: [
 | 
			
		||||
    'content_translation_ui_skip' => TRUE,
 | 
			
		||||
  ],
 | 
			
		||||
)]
 | 
			
		||||
class EntityTestTranslatableUISkip extends EntityTest {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,88 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\content_translation_test\Hook;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Access\AccessResultInterface;
 | 
			
		||||
use Drupal\Core\Form\FormStateInterface;
 | 
			
		||||
use Drupal\Core\Access\AccessResult;
 | 
			
		||||
use Drupal\Core\Session\AccountInterface;
 | 
			
		||||
use Drupal\Core\Entity\EntityInterface;
 | 
			
		||||
use Drupal\Core\Hook\Attribute\Hook;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Hook implementations for content_translation_test.
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationTestHooks {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Implements hook_entity_bundle_info_alter().
 | 
			
		||||
   */
 | 
			
		||||
  #[Hook('entity_bundle_info_alter')]
 | 
			
		||||
  public function entityBundleInfoAlter(&$bundles): void {
 | 
			
		||||
    // Store the initial status of the "translatable" property for the
 | 
			
		||||
    // "entity_test_mul" bundle.
 | 
			
		||||
    $translatable = !empty($bundles['entity_test_mul']['entity_test_mul']['translatable']);
 | 
			
		||||
    \Drupal::state()->set('content_translation_test.translatable', $translatable);
 | 
			
		||||
    // Make it translatable if Content Translation did not. This will make the
 | 
			
		||||
    // entity object translatable even if it is disabled in Content Translation
 | 
			
		||||
    // settings.
 | 
			
		||||
    if (!$translatable) {
 | 
			
		||||
      $bundles['entity_test_mul']['entity_test_mul']['translatable'] = TRUE;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Implements hook_entity_access().
 | 
			
		||||
   */
 | 
			
		||||
  #[Hook('entity_access')]
 | 
			
		||||
  public function entityAccess(EntityInterface $entity, $operation, AccountInterface $account): AccessResultInterface {
 | 
			
		||||
    $access = \Drupal::state()->get('content_translation.entity_access.' . $entity->getEntityTypeId());
 | 
			
		||||
    if (!empty($access[$operation])) {
 | 
			
		||||
      return AccessResult::allowed();
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      return AccessResult::neutral();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Implements hook_form_BASE_FORM_ID_alter().
 | 
			
		||||
   *
 | 
			
		||||
   * Adds a textfield to node forms based on a request parameter.
 | 
			
		||||
   */
 | 
			
		||||
  #[Hook('form_node_form_alter')]
 | 
			
		||||
  public function formNodeFormAlter(&$form, FormStateInterface $form_state, $form_id) : void {
 | 
			
		||||
    $langcode = $form_state->getFormObject()->getFormLangcode($form_state);
 | 
			
		||||
    if (in_array($langcode, ['en', 'fr']) && \Drupal::request()->get('test_field_only_en_fr')) {
 | 
			
		||||
      $form['test_field_only_en_fr'] = [
 | 
			
		||||
        '#type' => 'textfield',
 | 
			
		||||
        '#title' => 'Field only available on the english and french form',
 | 
			
		||||
      ];
 | 
			
		||||
      foreach (array_keys($form['actions']) as $action) {
 | 
			
		||||
        if ($action != 'preview' && isset($form['actions'][$action]['#type']) && $form['actions'][$action]['#type'] === 'submit') {
 | 
			
		||||
          $form['actions'][$action]['#submit'][] = [$this, 'formNodeFormSubmit'];
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Implements hook_entity_translation_delete().
 | 
			
		||||
   */
 | 
			
		||||
  #[Hook('entity_translation_delete')]
 | 
			
		||||
  public function entityTranslationDelete(EntityInterface $translation): void {
 | 
			
		||||
    \Drupal::state()->set('content_translation_test.translation_deleted', TRUE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Form submission handler for custom field added based on a request parameter.
 | 
			
		||||
   *
 | 
			
		||||
   * @see content_translation_test_form_node_article_form_alter()
 | 
			
		||||
   */
 | 
			
		||||
  public function formNodeFormSubmit($form, FormStateInterface $form_state): void {
 | 
			
		||||
    \Drupal::state()->set('test_field_only_en_fr', $form_state->getValue('test_field_only_en_fr'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,8 @@
 | 
			
		||||
name: 'Content translation test views'
 | 
			
		||||
type: module
 | 
			
		||||
description: 'Provides default views for views content translation tests.'
 | 
			
		||||
package: Testing
 | 
			
		||||
version: VERSION
 | 
			
		||||
dependencies:
 | 
			
		||||
  - drupal:content_translation
 | 
			
		||||
  - drupal:views
 | 
			
		||||
@ -0,0 +1,112 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  module:
 | 
			
		||||
    - content_translation
 | 
			
		||||
    - user
 | 
			
		||||
id: test_entity_translations_link
 | 
			
		||||
label: People
 | 
			
		||||
module: views
 | 
			
		||||
description: ''
 | 
			
		||||
tag: ''
 | 
			
		||||
base_table: users_field_data
 | 
			
		||||
base_field: uid
 | 
			
		||||
display:
 | 
			
		||||
  default:
 | 
			
		||||
    display_plugin: default
 | 
			
		||||
    id: default
 | 
			
		||||
    display_title: Default
 | 
			
		||||
    position: null
 | 
			
		||||
    display_options:
 | 
			
		||||
      access:
 | 
			
		||||
        type: none
 | 
			
		||||
      cache:
 | 
			
		||||
        type: tag
 | 
			
		||||
      query:
 | 
			
		||||
        type: views_query
 | 
			
		||||
      exposed_form:
 | 
			
		||||
        type: basic
 | 
			
		||||
        options:
 | 
			
		||||
          submit_button: Filter
 | 
			
		||||
          reset_button: true
 | 
			
		||||
          reset_button_label: Reset
 | 
			
		||||
      pager:
 | 
			
		||||
        type: full
 | 
			
		||||
        options:
 | 
			
		||||
          items_per_page: 50
 | 
			
		||||
      style:
 | 
			
		||||
        type: table
 | 
			
		||||
        options:
 | 
			
		||||
          class: ''
 | 
			
		||||
          columns:
 | 
			
		||||
            name: name
 | 
			
		||||
            translation_link: translation_link
 | 
			
		||||
          default: created
 | 
			
		||||
      row:
 | 
			
		||||
        type: fields
 | 
			
		||||
      fields:
 | 
			
		||||
        name:
 | 
			
		||||
          id: name
 | 
			
		||||
          table: users_field_data
 | 
			
		||||
          field: name
 | 
			
		||||
          label: Username
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
          type: user_name
 | 
			
		||||
          entity_type: user
 | 
			
		||||
          entity_field: name
 | 
			
		||||
        translation_link:
 | 
			
		||||
          id: translation_link
 | 
			
		||||
          table: users
 | 
			
		||||
          field: translation_link
 | 
			
		||||
          label: 'Translation link'
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          text: Translate
 | 
			
		||||
          plugin_id: content_translation_link
 | 
			
		||||
          entity_type: user
 | 
			
		||||
      filters:
 | 
			
		||||
        uid_raw:
 | 
			
		||||
          id: uid_raw
 | 
			
		||||
          table: users_field_data
 | 
			
		||||
          field: uid_raw
 | 
			
		||||
          operator: '!='
 | 
			
		||||
          value:
 | 
			
		||||
            value: '0'
 | 
			
		||||
          group: 1
 | 
			
		||||
          exposed: false
 | 
			
		||||
          plugin_id: numeric
 | 
			
		||||
          entity_type: user
 | 
			
		||||
      sorts:
 | 
			
		||||
        created:
 | 
			
		||||
          id: created
 | 
			
		||||
          table: users_field_data
 | 
			
		||||
          field: created
 | 
			
		||||
          order: DESC
 | 
			
		||||
          plugin_id: date
 | 
			
		||||
          entity_type: user
 | 
			
		||||
          entity_field: created
 | 
			
		||||
      title: People
 | 
			
		||||
      empty:
 | 
			
		||||
        area:
 | 
			
		||||
          id: area
 | 
			
		||||
          table: views
 | 
			
		||||
          field: area
 | 
			
		||||
          empty: true
 | 
			
		||||
          content:
 | 
			
		||||
            value: 'No people available.'
 | 
			
		||||
            format: plain_text
 | 
			
		||||
          plugin_id: text
 | 
			
		||||
  page_1:
 | 
			
		||||
    display_plugin: page
 | 
			
		||||
    id: page_1
 | 
			
		||||
    display_title: Page
 | 
			
		||||
    position: null
 | 
			
		||||
    display_options:
 | 
			
		||||
      path: test-entity-translations-link
 | 
			
		||||
@ -0,0 +1,62 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the test content translation UI with the test entity.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 * @group #slow
 | 
			
		||||
 */
 | 
			
		||||
class ContentTestTranslationUITest extends ContentTranslationUITestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $testHTMLEscapeForAllLanguages = TRUE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultCacheContexts = [
 | 
			
		||||
    'languages:language_interface',
 | 
			
		||||
    'theme',
 | 
			
		||||
    'url.query_args:_wrapper_format',
 | 
			
		||||
    'user.permissions',
 | 
			
		||||
    'url.site',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    // Use the entity_test_mul as this has multilingual property support.
 | 
			
		||||
    $this->entityTypeId = 'entity_test_mul_changed';
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->doSetup();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getTranslatorPermissions(): array {
 | 
			
		||||
    return array_merge(parent::getTranslatorPermissions(), ['administer entity_test content', 'view test entity']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,128 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\Tests\content_translation\Traits\ContentTranslationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests that contextual links are available for content translation.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationContextualLinksTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentTranslationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The bundle being tested.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $bundle;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The content type being tested.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\node\Entity\NodeType
 | 
			
		||||
   */
 | 
			
		||||
  protected $contentType;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The 'translator' user to use during testing.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\user\UserInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $translator;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The enabled languages.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $langcodes;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['content_translation', 'contextual', 'node'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The profile to install as a basis for testing.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    // Set up an additional language.
 | 
			
		||||
    $this->langcodes = [\Drupal::languageManager()->getDefaultLanguage()->getId(), 'es'];
 | 
			
		||||
    static::createLanguageFromLangcode('es');
 | 
			
		||||
 | 
			
		||||
    // Create a content type.
 | 
			
		||||
    $this->bundle = $this->randomMachineName();
 | 
			
		||||
    $this->contentType = $this->drupalCreateContentType(['type' => $this->bundle]);
 | 
			
		||||
 | 
			
		||||
    // Add a field to the content type. The field is not yet translatable.
 | 
			
		||||
    FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => 'field_test_text',
 | 
			
		||||
      'entity_type' => 'node',
 | 
			
		||||
      'type' => 'text',
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'entity_type' => 'node',
 | 
			
		||||
      'field_name' => 'field_test_text',
 | 
			
		||||
      'bundle' => $this->bundle,
 | 
			
		||||
      'label' => 'Test text-field',
 | 
			
		||||
    ])->save();
 | 
			
		||||
    $this->container->get('entity_display.repository')
 | 
			
		||||
      ->getFormDisplay('node', $this->bundle)
 | 
			
		||||
      ->setComponent('field_test_text', [
 | 
			
		||||
        'type' => 'text_textfield',
 | 
			
		||||
        'weight' => 0,
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    // Create a translator user.
 | 
			
		||||
    $permissions = [
 | 
			
		||||
      'access contextual links',
 | 
			
		||||
      'administer nodes',
 | 
			
		||||
      "edit any $this->bundle content",
 | 
			
		||||
      'translate any entity',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->translator = $this->drupalCreateUser($permissions);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that a contextual link is available for translating a node.
 | 
			
		||||
   */
 | 
			
		||||
  public function testContentTranslationContextualLinks(): void {
 | 
			
		||||
    // Create a node.
 | 
			
		||||
    $title = $this->randomString();
 | 
			
		||||
    $this->drupalCreateNode(['type' => $this->bundle, 'title' => $title, 'langcode' => 'en']);
 | 
			
		||||
    $node = $this->drupalGetNodeByTitle($title);
 | 
			
		||||
 | 
			
		||||
    static::enableContentTranslation('node', $this->bundle);
 | 
			
		||||
 | 
			
		||||
    // Check that the link leads to the translate page.
 | 
			
		||||
    $this->drupalLogin($this->translator);
 | 
			
		||||
    $translate_link = 'node/' . $node->id() . '/translations';
 | 
			
		||||
    $this->drupalGet($translate_link);
 | 
			
		||||
    $this->assertSession()->pageTextContains('Translations of ' . $node->label());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,78 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test disabling content translation module.
 | 
			
		||||
 *
 | 
			
		||||
 * @covers \Drupal\language\Form\ContentLanguageSettingsForm
 | 
			
		||||
 * @covers ::_content_translation_form_language_content_settings_form_alter
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationDisableSettingTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'menu_link_content',
 | 
			
		||||
    'language',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that entity schemas are up-to-date after enabling translation.
 | 
			
		||||
   */
 | 
			
		||||
  public function testDisableSetting(): void {
 | 
			
		||||
    // Define selectors.
 | 
			
		||||
    $group_checkbox = 'entity_types[menu_link_content]';
 | 
			
		||||
    $translatable_checkbox = 'settings[menu_link_content][menu_link_content][translatable]';
 | 
			
		||||
    $language_alterable = 'settings[menu_link_content][menu_link_content][settings][language][language_alterable]';
 | 
			
		||||
 | 
			
		||||
    $user = $this->drupalCreateUser([
 | 
			
		||||
      'administer site configuration',
 | 
			
		||||
      'administer content translation',
 | 
			
		||||
      'create content translations',
 | 
			
		||||
      'administer languages',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($user);
 | 
			
		||||
 | 
			
		||||
    $assert = $this->assertSession();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('admin/config/regional/content-language');
 | 
			
		||||
 | 
			
		||||
    $assert->checkboxNotChecked('entity_types[menu_link_content]');
 | 
			
		||||
 | 
			
		||||
    $edit = [
 | 
			
		||||
      $group_checkbox => TRUE,
 | 
			
		||||
      $translatable_checkbox => TRUE,
 | 
			
		||||
      $language_alterable => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save configuration');
 | 
			
		||||
 | 
			
		||||
    $assert->statusMessageContains('Settings successfully updated.', 'status');
 | 
			
		||||
 | 
			
		||||
    $assert->checkboxChecked($group_checkbox);
 | 
			
		||||
 | 
			
		||||
    $edit = [
 | 
			
		||||
      $group_checkbox => FALSE,
 | 
			
		||||
      $translatable_checkbox => TRUE,
 | 
			
		||||
      $language_alterable => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save configuration');
 | 
			
		||||
 | 
			
		||||
    $assert->statusMessageContains('Settings successfully updated.', 'status');
 | 
			
		||||
 | 
			
		||||
    $assert->checkboxNotChecked($group_checkbox);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,91 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\user\Entity\Role;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test enabling content translation module.
 | 
			
		||||
 *
 | 
			
		||||
 * @covers \Drupal\language\Form\ContentLanguageSettingsForm
 | 
			
		||||
 * @covers ::_content_translation_form_language_content_settings_form_alter
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationEnableTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['entity_test', 'menu_link_content', 'node'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that entity schemas are up-to-date after enabling translation.
 | 
			
		||||
   */
 | 
			
		||||
  public function testEnable(): void {
 | 
			
		||||
    $this->rootUser = $this->drupalCreateUser([
 | 
			
		||||
      'administer modules',
 | 
			
		||||
      'administer site configuration',
 | 
			
		||||
      'administer content types',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($this->rootUser);
 | 
			
		||||
    // Enable modules and make sure the related config entity type definitions
 | 
			
		||||
    // are installed.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'modules[content_translation][enable]' => TRUE,
 | 
			
		||||
      'modules[language][enable]' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('admin/modules');
 | 
			
		||||
    $this->submitForm($edit, 'Install');
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
 | 
			
		||||
    // Status messages are shown.
 | 
			
		||||
    $this->assertSession()->statusMessageContains('This site has only a single language enabled. Add at least one more language in order to translate content.', 'warning');
 | 
			
		||||
    $this->assertSession()->statusMessageContains('Enable translation for content types, taxonomy vocabularies, accounts, or any other element you wish to translate.', 'warning');
 | 
			
		||||
 | 
			
		||||
    // No pending updates should be available.
 | 
			
		||||
    $this->drupalGet('admin/reports/status');
 | 
			
		||||
    $this->assertSession()->elementTextEquals('css', "details.system-status-report__entry summary:contains('Entity/field definitions') + div", 'Up to date');
 | 
			
		||||
 | 
			
		||||
    $this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), [
 | 
			
		||||
      'administer content translation',
 | 
			
		||||
      'administer languages',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalGet('admin/config/regional/content-language');
 | 
			
		||||
    // The node entity type should not be an option because it has no bundles.
 | 
			
		||||
    $this->assertSession()->responseNotContains('entity_types[node]');
 | 
			
		||||
    // Enable content translation on entity types that have will have a
 | 
			
		||||
    // content_translation_uid.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'entity_types[menu_link_content]' => TRUE,
 | 
			
		||||
      'settings[menu_link_content][menu_link_content][translatable]' => TRUE,
 | 
			
		||||
      'entity_types[entity_test_mul]' => TRUE,
 | 
			
		||||
      'settings[entity_test_mul][entity_test_mul][translatable]' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save configuration');
 | 
			
		||||
 | 
			
		||||
    // No pending updates should be available.
 | 
			
		||||
    $this->drupalGet('admin/reports/status');
 | 
			
		||||
    $this->assertSession()->elementTextEquals('css', "details.system-status-report__entry summary:contains('Entity/field definitions') + div", 'Up to date');
 | 
			
		||||
 | 
			
		||||
    // Create a node type and check the content translation settings are now
 | 
			
		||||
    // available for nodes.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'name' => 'foo',
 | 
			
		||||
      'title_label' => 'title for foo',
 | 
			
		||||
      'type' => 'foo',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('admin/structure/types/add');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->drupalGet('admin/config/regional/content-language');
 | 
			
		||||
    $this->assertSession()->responseContains('entity_types[node]');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,77 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the content translation behaviors on entity bundle UI.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationEntityBundleUITest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'node',
 | 
			
		||||
    'comment',
 | 
			
		||||
    'field_ui',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $user = $this->drupalCreateUser([
 | 
			
		||||
      'access administration pages',
 | 
			
		||||
      'administer languages',
 | 
			
		||||
      'administer content translation',
 | 
			
		||||
      'administer content types',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($user);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests content types default translation behavior.
 | 
			
		||||
   */
 | 
			
		||||
  public function testContentTypeUI(): void {
 | 
			
		||||
    // Create first content type.
 | 
			
		||||
    $this->drupalCreateContentType(['type' => 'article']);
 | 
			
		||||
    // Enable content translation.
 | 
			
		||||
    $edit = ['language_configuration[content_translation]' => TRUE];
 | 
			
		||||
    $this->drupalGet('admin/structure/types/manage/article');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
 | 
			
		||||
    // Make sure add page does not inherit translation configuration from first
 | 
			
		||||
    // content type.
 | 
			
		||||
    $this->drupalGet('admin/structure/types/add');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-language-configuration-content-translation');
 | 
			
		||||
 | 
			
		||||
    // Create second content type and set content translation.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'name' => 'Page',
 | 
			
		||||
      'type' => 'page',
 | 
			
		||||
      'language_configuration[content_translation]' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('admin/structure/types/add');
 | 
			
		||||
    $this->submitForm($edit, 'Save and manage fields');
 | 
			
		||||
 | 
			
		||||
    // Make sure the settings are saved when creating the content type.
 | 
			
		||||
    $this->drupalGet('admin/structure/types/manage/page');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-language-configuration-content-translation');
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,158 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\content_translation\Traits\ContentTranslationTestTrait;
 | 
			
		||||
use Drupal\Tests\image\Kernel\ImageFieldCreationTrait;
 | 
			
		||||
use Drupal\Tests\node\Functional\NodeTestBase;
 | 
			
		||||
use Drupal\Tests\TestFileCreationTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the content translation language that is set.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationLanguageChangeTest extends NodeTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentTranslationTestTrait;
 | 
			
		||||
  use ImageFieldCreationTrait;
 | 
			
		||||
  use TestFileCreationTrait {
 | 
			
		||||
    getTestFiles as drupalGetTestFiles;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'content_translation_test',
 | 
			
		||||
    'node',
 | 
			
		||||
    'block',
 | 
			
		||||
    'field_ui',
 | 
			
		||||
    'image',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $langcodes = ['de', 'fr'];
 | 
			
		||||
    foreach ($langcodes as $langcode) {
 | 
			
		||||
      static::createLanguageFromLangcode($langcode);
 | 
			
		||||
    }
 | 
			
		||||
    $this->drupalPlaceBlock('local_tasks_block');
 | 
			
		||||
    $user = $this->drupalCreateUser([
 | 
			
		||||
      'administer site configuration',
 | 
			
		||||
      'administer nodes',
 | 
			
		||||
      'create article content',
 | 
			
		||||
      'edit any article content',
 | 
			
		||||
      'delete any article content',
 | 
			
		||||
      'administer content translation',
 | 
			
		||||
      'translate any entity',
 | 
			
		||||
      'create content translations',
 | 
			
		||||
      'administer languages',
 | 
			
		||||
      'administer content types',
 | 
			
		||||
      'administer node fields',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($user);
 | 
			
		||||
 | 
			
		||||
    // Enable translations for article.
 | 
			
		||||
    $this->enableContentTranslation('node', 'article');
 | 
			
		||||
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
 | 
			
		||||
    $this->createImageField('field_image_field', 'node', 'article');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that the source language is properly set when changing.
 | 
			
		||||
   */
 | 
			
		||||
  public function testLanguageChange(): void {
 | 
			
		||||
    // Create a node in English.
 | 
			
		||||
    $this->drupalGet('node/add/article');
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'english_title',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
 | 
			
		||||
    // Create a translation in French.
 | 
			
		||||
    $this->clickLink('Translate');
 | 
			
		||||
    $this->clickLink('Add');
 | 
			
		||||
    $this->submitForm([], 'Save (this translation)');
 | 
			
		||||
    $this->clickLink('Translate');
 | 
			
		||||
 | 
			
		||||
    // Edit English translation.
 | 
			
		||||
    $this->clickLink('Edit', 1);
 | 
			
		||||
    // Upload and image after changing the node language.
 | 
			
		||||
    $images = $this->drupalGetTestFiles('image')[1];
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'langcode[0][value]' => 'de',
 | 
			
		||||
      'files[field_image_field_0]' => $images->uri,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Upload');
 | 
			
		||||
    $this->submitForm(['field_image_field[0][alt]' => 'alternative_text'], 'Save (this translation)');
 | 
			
		||||
 | 
			
		||||
    // Check that the translation languages are correct.
 | 
			
		||||
    $node = $this->getNodeByTitle('english_title');
 | 
			
		||||
    $translation_languages = $node->getTranslationLanguages();
 | 
			
		||||
    $this->assertArrayHasKey('fr', $translation_languages);
 | 
			
		||||
    $this->assertArrayHasKey('de', $translation_languages);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that title does not change on ajax call with new language value.
 | 
			
		||||
   */
 | 
			
		||||
  public function testTitleDoesNotChangesOnChangingLanguageWidgetAndTriggeringAjaxCall(): void {
 | 
			
		||||
    // Create a node in English.
 | 
			
		||||
    $this->drupalGet('node/add/article', ['query' => ['test_field_only_en_fr' => 1]]);
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'english_title',
 | 
			
		||||
      'test_field_only_en_fr' => 'node created',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertEquals('node created', \Drupal::state()->get('test_field_only_en_fr'));
 | 
			
		||||
 | 
			
		||||
    // Create a translation in French.
 | 
			
		||||
    $this->clickLink('Translate');
 | 
			
		||||
    $this->clickLink('Add');
 | 
			
		||||
    $this->submitForm([], 'Save (this translation)');
 | 
			
		||||
    $this->clickLink('Translate');
 | 
			
		||||
 | 
			
		||||
    // Edit English translation.
 | 
			
		||||
    $node = $this->getNodeByTitle('english_title');
 | 
			
		||||
    $this->drupalGet('node/' . $node->id() . '/edit');
 | 
			
		||||
    // Test the expected title when loading the form.
 | 
			
		||||
    $this->assertSession()->titleEquals('Edit Article english_title | Drupal');
 | 
			
		||||
    // Upload and image after changing the node language.
 | 
			
		||||
    $images = $this->drupalGetTestFiles('image')[1];
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'langcode[0][value]' => 'de',
 | 
			
		||||
      'files[field_image_field_0]' => $images->uri,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Upload');
 | 
			
		||||
    // Test the expected title after triggering an ajax call with a new
 | 
			
		||||
    // language selected.
 | 
			
		||||
    $this->assertSession()->titleEquals('Edit Article english_title | Drupal');
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'langcode[0][value]' => 'en',
 | 
			
		||||
      'field_image_field[0][alt]' => 'alternative_text',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
 | 
			
		||||
    // Check that the translation languages are correct.
 | 
			
		||||
    $node = $this->getNodeByTitle('english_title');
 | 
			
		||||
    $translation_languages = $node->getTranslationLanguages();
 | 
			
		||||
    $this->assertArrayHasKey('fr', $translation_languages);
 | 
			
		||||
    $this->assertArrayNotHasKey('de', $translation_languages);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,163 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\EntityInterface;
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestMul;
 | 
			
		||||
use Drupal\content_translation_test\Entity\EntityTestTranslatableNoUISkip;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests whether canonical link tags are present for content entities.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationLinkTagTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'content_translation_test',
 | 
			
		||||
    'language',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The added languages.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string[]
 | 
			
		||||
   */
 | 
			
		||||
  protected $langcodes;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    // Set up user.
 | 
			
		||||
    $user = $this->drupalCreateUser([
 | 
			
		||||
      'view test entity',
 | 
			
		||||
      'view test entity translations',
 | 
			
		||||
      'administer entity_test content',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($user);
 | 
			
		||||
 | 
			
		||||
    // Add additional languages.
 | 
			
		||||
    $this->langcodes = ['it', 'fr'];
 | 
			
		||||
    foreach ($this->langcodes as $langcode) {
 | 
			
		||||
      ConfigurableLanguage::createFromLangcode($langcode)->save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Rebuild the container so that the new languages are picked up by services
 | 
			
		||||
    // that hold a list of languages.
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Create a test entity with translations.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Entity\EntityInterface
 | 
			
		||||
   *   An entity with translations.
 | 
			
		||||
   */
 | 
			
		||||
  protected function createTranslatableEntity(): EntityInterface {
 | 
			
		||||
    $entity = EntityTestMul::create(['label' => $this->randomString()]);
 | 
			
		||||
 | 
			
		||||
    // Create translations for non default languages.
 | 
			
		||||
    foreach ($this->langcodes as $langcode) {
 | 
			
		||||
      $entity->addTranslation($langcode, ['label' => $this->randomString()]);
 | 
			
		||||
    }
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    return $entity;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests alternate link tag found for entity types with canonical links.
 | 
			
		||||
   */
 | 
			
		||||
  public function testCanonicalAlternateTags(): void {
 | 
			
		||||
    /** @var \Drupal\Core\Language\LanguageManagerInterface $languageManager */
 | 
			
		||||
    $languageManager = $this->container->get('language_manager');
 | 
			
		||||
    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager */
 | 
			
		||||
    $entityTypeManager = $this->container->get('entity_type.manager');
 | 
			
		||||
 | 
			
		||||
    $definition = $entityTypeManager->getDefinition('entity_test_mul');
 | 
			
		||||
    $this->assertTrue($definition->hasLinkTemplate('canonical'), 'Canonical link template found for entity_test.');
 | 
			
		||||
 | 
			
		||||
    $entity = $this->createTranslatableEntity();
 | 
			
		||||
    $url_base = $entity->toUrl('canonical')
 | 
			
		||||
      ->setAbsolute();
 | 
			
		||||
 | 
			
		||||
    $langcodes_all = $this->langcodes;
 | 
			
		||||
    $langcodes_all[] = $languageManager
 | 
			
		||||
      ->getDefaultLanguage()
 | 
			
		||||
      ->getId();
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Url[] $urls */
 | 
			
		||||
    $urls = array_map(
 | 
			
		||||
      function ($langcode) use ($url_base, $languageManager) {
 | 
			
		||||
        $url = clone $url_base;
 | 
			
		||||
        return $url
 | 
			
		||||
          ->setOption('language', $languageManager->getLanguage($langcode));
 | 
			
		||||
      },
 | 
			
		||||
      array_combine($langcodes_all, $langcodes_all)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Ensure link tags for all languages are found on each language variation
 | 
			
		||||
    // page of an entity.
 | 
			
		||||
    foreach ($urls as $url) {
 | 
			
		||||
      $this->drupalGet($url);
 | 
			
		||||
      foreach ($urls as $langcode_alternate => $url_alternate) {
 | 
			
		||||
        $this->assertSession()->elementAttributeContains('xpath', "head/link[@rel='alternate' and @hreflang='$langcode_alternate']", 'href', $url_alternate->toString());
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Configure entity path as a front page.
 | 
			
		||||
    $entity_canonical = '/entity_test_mul/manage/' . $entity->id();
 | 
			
		||||
    $this->config('system.site')->set('page.front', $entity_canonical)->save();
 | 
			
		||||
 | 
			
		||||
    // Tests hreflang when using entities as a front page.
 | 
			
		||||
    foreach ($urls as $url) {
 | 
			
		||||
      $this->drupalGet($url);
 | 
			
		||||
      foreach ($entity->getTranslationLanguages() as $language) {
 | 
			
		||||
        $frontpage_path = Url::fromRoute('<front>', [], [
 | 
			
		||||
          'absolute' => TRUE,
 | 
			
		||||
          'language' => $language,
 | 
			
		||||
        ])->toString();
 | 
			
		||||
        $this->assertSession()->elementAttributeContains('xpath', "head/link[@rel='alternate' and @hreflang='{$language->getId()}']", 'href', $frontpage_path);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests alternate link tag missing for entity types without canonical links.
 | 
			
		||||
   */
 | 
			
		||||
  public function testCanonicalAlternateTagsMissing(): void {
 | 
			
		||||
    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager */
 | 
			
		||||
    $entityTypeManager = $this->container->get('entity_type.manager');
 | 
			
		||||
 | 
			
		||||
    $definition = $entityTypeManager->getDefinition('entity_test_translatable_no_skip');
 | 
			
		||||
    // Ensure 'canonical' link template does not exist, in case it is added in
 | 
			
		||||
    // the future.
 | 
			
		||||
    $this->assertFalse($definition->hasLinkTemplate('canonical'), 'Canonical link template does not exist for entity_test_translatable_no_skip entity.');
 | 
			
		||||
 | 
			
		||||
    $entity = EntityTestTranslatableNoUISkip::create();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $this->drupalGet($entity->toUrl('edit-form'));
 | 
			
		||||
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->elementNotExists('xpath', '//link[@rel="alternate" and @hreflang]');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,156 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the Content Translation metadata fields handling.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationMetadataFieldsTest extends ContentTranslationTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The entity type being tested.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityTypeId = 'node';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The bundle being tested.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $bundle = 'article';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['language', 'content_translation', 'node'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setupBundle(): void {
 | 
			
		||||
    parent::setupBundle();
 | 
			
		||||
    $this->createContentType(['type' => $this->bundle]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->doSetup();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests skipping setting non translatable metadata fields.
 | 
			
		||||
   */
 | 
			
		||||
  public function testSkipUntranslatable(): void {
 | 
			
		||||
    $this->drupalLogin($this->translator);
 | 
			
		||||
    $fields = \Drupal::service('entity_field.manager')->getFieldDefinitions($this->entityTypeId, $this->bundle);
 | 
			
		||||
 | 
			
		||||
    // Turn off translatability for the metadata fields on the current bundle.
 | 
			
		||||
    $metadata_fields = ['created', 'changed', 'uid', 'status'];
 | 
			
		||||
    foreach ($metadata_fields as $field_name) {
 | 
			
		||||
      $fields[$field_name]
 | 
			
		||||
        ->getConfig($this->bundle)
 | 
			
		||||
        ->setTranslatable(FALSE)
 | 
			
		||||
        ->save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create a new test entity with original values in the default language.
 | 
			
		||||
    $default_langcode = $this->langcodes[0];
 | 
			
		||||
    $entity_id = $this->createEntity(['title' => $this->randomString()], $default_langcode);
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage($this->entityTypeId);
 | 
			
		||||
    $storage->resetCache();
 | 
			
		||||
    $entity = $storage->load($entity_id);
 | 
			
		||||
 | 
			
		||||
    // Add a content translation.
 | 
			
		||||
    $langcode = 'it';
 | 
			
		||||
    $values = $entity->toArray();
 | 
			
		||||
    // Apply a default value for the metadata fields.
 | 
			
		||||
    foreach ($metadata_fields as $field_name) {
 | 
			
		||||
      unset($values[$field_name]);
 | 
			
		||||
    }
 | 
			
		||||
    $entity->addTranslation($langcode, $values);
 | 
			
		||||
 | 
			
		||||
    $metadata_source_translation = $this->manager->getTranslationMetadata($entity->getTranslation($default_langcode));
 | 
			
		||||
    $metadata_target_translation = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
 | 
			
		||||
 | 
			
		||||
    $created_time = $metadata_source_translation->getCreatedTime();
 | 
			
		||||
    $changed_time = $metadata_source_translation->getChangedTime();
 | 
			
		||||
    $published = $metadata_source_translation->isPublished();
 | 
			
		||||
    $author = $metadata_source_translation->getAuthor();
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals($created_time, $metadata_target_translation->getCreatedTime(), 'Metadata created field has the same value for both translations.');
 | 
			
		||||
    $this->assertEquals($changed_time, $metadata_target_translation->getChangedTime(), 'Metadata changed field has the same value for both translations.');
 | 
			
		||||
    $this->assertEquals($published, $metadata_target_translation->isPublished(), 'Metadata published field has the same value for both translations.');
 | 
			
		||||
    $this->assertEquals($author->id(), $metadata_target_translation->getAuthor()->id(), 'Metadata author field has the same value for both translations.');
 | 
			
		||||
 | 
			
		||||
    $metadata_target_translation->setCreatedTime(time() + 50);
 | 
			
		||||
    $metadata_target_translation->setChangedTime(time() + 50);
 | 
			
		||||
    $metadata_target_translation->setPublished(TRUE);
 | 
			
		||||
    $metadata_target_translation->setAuthor($this->editor);
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals($created_time, $metadata_target_translation->getCreatedTime(), 'Metadata created field correctly not updated');
 | 
			
		||||
    $this->assertEquals($changed_time, $metadata_target_translation->getChangedTime(), 'Metadata changed field correctly not updated');
 | 
			
		||||
    $this->assertEquals($published, $metadata_target_translation->isPublished(), 'Metadata published field correctly not updated');
 | 
			
		||||
    $this->assertEquals($author->id(), $metadata_target_translation->getAuthor()->id(), 'Metadata author field correctly not updated');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests setting translatable metadata fields.
 | 
			
		||||
   */
 | 
			
		||||
  public function testSetTranslatable(): void {
 | 
			
		||||
    $this->drupalLogin($this->translator);
 | 
			
		||||
    $fields = \Drupal::service('entity_field.manager')->getFieldDefinitions($this->entityTypeId, $this->bundle);
 | 
			
		||||
 | 
			
		||||
    // Turn off translatability for the metadata fields on the current bundle.
 | 
			
		||||
    $metadata_fields = ['created', 'changed', 'uid', 'status'];
 | 
			
		||||
    foreach ($metadata_fields as $field_name) {
 | 
			
		||||
      $fields[$field_name]
 | 
			
		||||
        ->getConfig($this->bundle)
 | 
			
		||||
        ->setTranslatable(TRUE)
 | 
			
		||||
        ->save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create a new test entity with original values in the default language.
 | 
			
		||||
    $default_langcode = $this->langcodes[0];
 | 
			
		||||
    $entity_id = $this->createEntity(['title' => $this->randomString(), 'status' => FALSE], $default_langcode);
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage($this->entityTypeId);
 | 
			
		||||
    $storage->resetCache();
 | 
			
		||||
    $entity = $storage->load($entity_id);
 | 
			
		||||
 | 
			
		||||
    // Add a content translation.
 | 
			
		||||
    $langcode = 'it';
 | 
			
		||||
    $values = $entity->toArray();
 | 
			
		||||
    // Apply a default value for the metadata fields.
 | 
			
		||||
    foreach ($metadata_fields as $field_name) {
 | 
			
		||||
      unset($values[$field_name]);
 | 
			
		||||
    }
 | 
			
		||||
    $entity->addTranslation($langcode, $values);
 | 
			
		||||
 | 
			
		||||
    $metadata_source_translation = $this->manager->getTranslationMetadata($entity->getTranslation($default_langcode));
 | 
			
		||||
    $metadata_target_translation = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
 | 
			
		||||
 | 
			
		||||
    $metadata_target_translation->setCreatedTime(time() + 50);
 | 
			
		||||
    $metadata_target_translation->setChangedTime(time() + 50);
 | 
			
		||||
    $metadata_target_translation->setPublished(TRUE);
 | 
			
		||||
    $metadata_target_translation->setAuthor($this->editor);
 | 
			
		||||
 | 
			
		||||
    $this->assertNotEquals($metadata_source_translation->getCreatedTime(), $metadata_target_translation->getCreatedTime(), 'Metadata created field correctly different on both translations.');
 | 
			
		||||
    $this->assertNotEquals($metadata_source_translation->getChangedTime(), $metadata_target_translation->getChangedTime(), 'Metadata changed field correctly different on both translations.');
 | 
			
		||||
    $this->assertNotEquals($metadata_source_translation->isPublished(), $metadata_target_translation->isPublished(), 'Metadata published field correctly different on both translations.');
 | 
			
		||||
    $this->assertNotEquals($metadata_source_translation->getAuthor()->id(), $metadata_target_translation->getAuthor()->id(), 'Metadata author field correctly different on both translations.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,173 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests that new translations do not delete existing ones.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationNewTranslationWithExistingRevisionsTest extends ContentTranslationPendingRevisionTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'content_translation_test',
 | 
			
		||||
    'language',
 | 
			
		||||
    'node',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->doSetup();
 | 
			
		||||
    $this->enableContentModeration();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests a translation with a draft is not deleted.
 | 
			
		||||
   */
 | 
			
		||||
  public function testDraftTranslationIsNotDeleted(): void {
 | 
			
		||||
    $this->drupalLogin($this->translator);
 | 
			
		||||
 | 
			
		||||
    // Create a test node.
 | 
			
		||||
    $values = [
 | 
			
		||||
      'title' => "Test EN",
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ];
 | 
			
		||||
    $id = $this->createEntity($values, 'en');
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
 | 
			
		||||
    $entity = $this->storage->load($id);
 | 
			
		||||
 | 
			
		||||
    // Add a published translation.
 | 
			
		||||
    $add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add",
 | 
			
		||||
      [
 | 
			
		||||
        $entity->getEntityTypeId() => $id,
 | 
			
		||||
        'source' => 'en',
 | 
			
		||||
        'target' => 'it',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'language' => ConfigurableLanguage::load('it'),
 | 
			
		||||
        'absolute' => FALSE,
 | 
			
		||||
      ]
 | 
			
		||||
    );
 | 
			
		||||
    $this->drupalGet($add_translation_url);
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => "Test IT",
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    $it_revision = $this->loadRevisionTranslation($entity, 'it');
 | 
			
		||||
 | 
			
		||||
    // Add a draft translation.
 | 
			
		||||
    $this->drupalGet($this->getEditUrl($it_revision));
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => "Test IT 2",
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
 | 
			
		||||
    // Add a new draft translation.
 | 
			
		||||
    $add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add",
 | 
			
		||||
      [
 | 
			
		||||
        $entity->getEntityTypeId() => $id,
 | 
			
		||||
        'source' => 'en',
 | 
			
		||||
        'target' => 'fr',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'language' => ConfigurableLanguage::load('fr'),
 | 
			
		||||
        'absolute' => FALSE,
 | 
			
		||||
      ]
 | 
			
		||||
    );
 | 
			
		||||
    $this->drupalGet($add_translation_url);
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => "Test FR",
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    // Check the first translation still exists.
 | 
			
		||||
    $entity = $this->storage->loadUnchanged($id);
 | 
			
		||||
    $this->assertTrue($entity->hasTranslation('it'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test translation delete hooks are not invoked.
 | 
			
		||||
   */
 | 
			
		||||
  public function testCreatingNewDraftDoesNotInvokeDeleteHook(): void {
 | 
			
		||||
    $this->drupalLogin($this->translator);
 | 
			
		||||
 | 
			
		||||
    // Create a test node.
 | 
			
		||||
    $values = [
 | 
			
		||||
      'title' => "Test EN",
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ];
 | 
			
		||||
    $id = $this->createEntity($values, 'en');
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
 | 
			
		||||
    $entity = $this->storage->load($id);
 | 
			
		||||
 | 
			
		||||
    // Add a published translation.
 | 
			
		||||
    $add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add",
 | 
			
		||||
      [
 | 
			
		||||
        $entity->getEntityTypeId() => $id,
 | 
			
		||||
        'source' => 'en',
 | 
			
		||||
        'target' => 'it',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'language' => ConfigurableLanguage::load('it'),
 | 
			
		||||
        'absolute' => FALSE,
 | 
			
		||||
      ]
 | 
			
		||||
    );
 | 
			
		||||
    $this->drupalGet($add_translation_url);
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => "Test IT",
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    $it_revision = $this->loadRevisionTranslation($entity, 'it');
 | 
			
		||||
 | 
			
		||||
    // Add a draft translation.
 | 
			
		||||
    $this->drupalGet($this->getEditUrl($it_revision));
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => "Test IT 2",
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    // Add a new draft translation.
 | 
			
		||||
    $add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add",
 | 
			
		||||
      [
 | 
			
		||||
        $entity->getEntityTypeId() => $id,
 | 
			
		||||
        'source' => 'en',
 | 
			
		||||
        'target' => 'fr',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'language' => ConfigurableLanguage::load('fr'),
 | 
			
		||||
        'absolute' => FALSE,
 | 
			
		||||
      ]
 | 
			
		||||
    );
 | 
			
		||||
    $this->drupalGet($add_translation_url);
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => "Test FR",
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    // If the translation delete hook was incorrectly invoked, the state
 | 
			
		||||
    // variable would be set.
 | 
			
		||||
    $this->assertNull($this->container->get('state')->get('content_translation_test.translation_deleted'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,171 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\language\Traits\LanguageTestTrait;
 | 
			
		||||
use Drupal\Tests\node\Functional\NodeTestBase;
 | 
			
		||||
use Drupal\user\Entity\Role;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the content translation operations available in the content listing.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationOperationsTest extends NodeTestBase {
 | 
			
		||||
 | 
			
		||||
  use LanguageTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A base user.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\user\Entity\User|false
 | 
			
		||||
   */
 | 
			
		||||
  protected $baseUser1;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A base user.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\user\Entity\User|false
 | 
			
		||||
   */
 | 
			
		||||
  protected $baseUser2;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'node',
 | 
			
		||||
    'views',
 | 
			
		||||
    'block',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    // Enable additional languages.
 | 
			
		||||
    $langcodes = ['es', 'ast'];
 | 
			
		||||
    foreach ($langcodes as $langcode) {
 | 
			
		||||
      static::createLanguageFromLangcode($langcode);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Enable translation for the current entity type and ensure the change is
 | 
			
		||||
    // picked up.
 | 
			
		||||
    \Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE);
 | 
			
		||||
 | 
			
		||||
    $this->baseUser1 = $this->drupalCreateUser(['access content overview']);
 | 
			
		||||
    $this->baseUser2 = $this->drupalCreateUser([
 | 
			
		||||
      'access content overview',
 | 
			
		||||
      'create content translations',
 | 
			
		||||
      'update content translations',
 | 
			
		||||
      'delete content translations',
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that the operation "Translate" is displayed in the content listing.
 | 
			
		||||
   */
 | 
			
		||||
  public function testOperationTranslateLink(): void {
 | 
			
		||||
    $node = $this->drupalCreateNode(['type' => 'article', 'langcode' => 'es']);
 | 
			
		||||
    // Verify no translation operation links are displayed for users without
 | 
			
		||||
    // permission.
 | 
			
		||||
    $this->drupalLogin($this->baseUser1);
 | 
			
		||||
    $this->drupalGet('admin/content');
 | 
			
		||||
    $this->assertSession()->linkByHrefNotExists('node/' . $node->id() . '/translations');
 | 
			
		||||
    $this->drupalLogout();
 | 
			
		||||
    // Verify there's a translation operation link for users with enough
 | 
			
		||||
    // permissions.
 | 
			
		||||
    $this->drupalLogin($this->baseUser2);
 | 
			
		||||
    $this->drupalGet('admin/content');
 | 
			
		||||
    $this->assertSession()->linkByHrefExists('node/' . $node->id() . '/translations');
 | 
			
		||||
 | 
			
		||||
    // Ensure that an unintended misconfiguration of permissions does not open
 | 
			
		||||
    // access to the translation form, see https://www.drupal.org/node/2558905.
 | 
			
		||||
    $this->drupalLogout();
 | 
			
		||||
    user_role_change_permissions(
 | 
			
		||||
      Role::AUTHENTICATED_ID,
 | 
			
		||||
      [
 | 
			
		||||
        'create content translations' => TRUE,
 | 
			
		||||
        'access content' => FALSE,
 | 
			
		||||
      ]
 | 
			
		||||
    );
 | 
			
		||||
    $this->drupalLogin($this->baseUser1);
 | 
			
		||||
    $this->drupalGet($node->toUrl('drupal:content-translation-overview'));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
 | 
			
		||||
    // Ensure that the translation overview is also not accessible when the user
 | 
			
		||||
    // has 'access content', but the node is not published.
 | 
			
		||||
    user_role_change_permissions(
 | 
			
		||||
      Role::AUTHENTICATED_ID,
 | 
			
		||||
      [
 | 
			
		||||
        'create content translations' => TRUE,
 | 
			
		||||
        'access content' => TRUE,
 | 
			
		||||
      ]
 | 
			
		||||
    );
 | 
			
		||||
    $node->setUnpublished()->save();
 | 
			
		||||
    $this->drupalGet($node->toUrl('drupal:content-translation-overview'));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
    $this->drupalLogout();
 | 
			
		||||
 | 
			
		||||
    // Ensure the 'Translate' local task does not show up anymore when disabling
 | 
			
		||||
    // translations for a content type.
 | 
			
		||||
    $node->setPublished()->save();
 | 
			
		||||
    user_role_change_permissions(
 | 
			
		||||
      Role::AUTHENTICATED_ID,
 | 
			
		||||
      [
 | 
			
		||||
        'administer content translation' => TRUE,
 | 
			
		||||
        'administer languages' => TRUE,
 | 
			
		||||
      ]
 | 
			
		||||
    );
 | 
			
		||||
    $this->drupalPlaceBlock('local_tasks_block');
 | 
			
		||||
    $this->drupalLogin($this->baseUser2);
 | 
			
		||||
    $this->drupalGet('node/' . $node->id());
 | 
			
		||||
    $this->assertSession()->linkByHrefExists('node/' . $node->id() . '/translations');
 | 
			
		||||
    static::disableBundleTranslation('node', 'article');
 | 
			
		||||
    $this->drupalGet('node/' . $node->id());
 | 
			
		||||
    $this->assertSession()->linkByHrefNotExists('node/' . $node->id() . '/translations');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the access to the overview page for translations.
 | 
			
		||||
   *
 | 
			
		||||
   * @see content_translation_translate_access()
 | 
			
		||||
   */
 | 
			
		||||
  public function testContentTranslationOverviewAccess(): void {
 | 
			
		||||
    $access_control_handler = \Drupal::entityTypeManager()->getAccessControlHandler('node');
 | 
			
		||||
    $user = $this->createUser(['create content translations', 'access content']);
 | 
			
		||||
    $this->drupalLogin($user);
 | 
			
		||||
 | 
			
		||||
    $node = $this->drupalCreateNode(['status' => FALSE, 'type' => 'article']);
 | 
			
		||||
    $this->assertFalse(content_translation_translate_access($node)->isAllowed());
 | 
			
		||||
    $access_control_handler->resetCache();
 | 
			
		||||
 | 
			
		||||
    $node->setPublished();
 | 
			
		||||
    $node->save();
 | 
			
		||||
    $this->assertTrue(content_translation_translate_access($node)->isAllowed());
 | 
			
		||||
    $access_control_handler->resetCache();
 | 
			
		||||
 | 
			
		||||
    user_role_change_permissions(
 | 
			
		||||
      Role::AUTHENTICATED_ID,
 | 
			
		||||
      [
 | 
			
		||||
        'access content' => FALSE,
 | 
			
		||||
      ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    $user = $this->createUser(['create content translations']);
 | 
			
		||||
    $this->drupalLogin($user);
 | 
			
		||||
    $this->assertFalse(content_translation_translate_access($node)->isAllowed());
 | 
			
		||||
    $access_control_handler->resetCache();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,101 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the "Flag as outdated" functionality with revision translations.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationOutdatedRevisionTranslationTest extends ContentTranslationPendingRevisionTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->doSetup();
 | 
			
		||||
    $this->enableContentModeration();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that outdated revision translations work correctly.
 | 
			
		||||
   */
 | 
			
		||||
  public function testFlagAsOutdatedHidden(): void {
 | 
			
		||||
    // Create a test node.
 | 
			
		||||
    $values = [
 | 
			
		||||
      'title' => 'Test 1.1 EN',
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ];
 | 
			
		||||
    $id = $this->createEntity($values, 'en');
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
 | 
			
		||||
    $entity = $this->storage->load($id);
 | 
			
		||||
 | 
			
		||||
    // Add a published Italian translation.
 | 
			
		||||
    $add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add",
 | 
			
		||||
      [
 | 
			
		||||
        $entity->getEntityTypeId() => $id,
 | 
			
		||||
        'source' => 'en',
 | 
			
		||||
        'target' => 'it',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'language' => ConfigurableLanguage::load('it'),
 | 
			
		||||
        'absolute' => FALSE,
 | 
			
		||||
      ]
 | 
			
		||||
    );
 | 
			
		||||
    $this->drupalGet($add_translation_url);
 | 
			
		||||
    $this->assertFlagWidget();
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'Test 1.2 IT',
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
 | 
			
		||||
    // Add a published French translation.
 | 
			
		||||
    $add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add",
 | 
			
		||||
      [
 | 
			
		||||
        $entity->getEntityTypeId() => $id,
 | 
			
		||||
        'source' => 'en',
 | 
			
		||||
        'target' => 'fr',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'language' => ConfigurableLanguage::load('fr'),
 | 
			
		||||
        'absolute' => FALSE,
 | 
			
		||||
      ]
 | 
			
		||||
    );
 | 
			
		||||
    $this->drupalGet($add_translation_url);
 | 
			
		||||
    $this->assertFlagWidget();
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'Test 1.3 FR',
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
 | 
			
		||||
    // Create an English draft.
 | 
			
		||||
    $entity = $this->storage->loadUnchanged($id);
 | 
			
		||||
    $en_edit_url = $this->getEditUrl($entity);
 | 
			
		||||
    $this->drupalGet($en_edit_url);
 | 
			
		||||
    $this->assertFlagWidget();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Checks whether the flag widget is displayed.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertFlagWidget(): void {
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Flag other translations as outdated');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Translations cannot be flagged as outdated when content is moderated.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,192 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\ContentEntityInterface;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base class for pending revision translation tests.
 | 
			
		||||
 */
 | 
			
		||||
abstract class ContentTranslationPendingRevisionTestBase extends ContentTranslationTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentTypeCreationTrait;
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'node',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The entity storage.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Entity\ContentEntityStorageInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $storage;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Permissions common to all test accounts.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string[]
 | 
			
		||||
   */
 | 
			
		||||
  protected $commonPermissions;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The current test account.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Session\AccountInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $currentAccount;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    $this->entityTypeId = 'node';
 | 
			
		||||
    $this->bundle = 'article';
 | 
			
		||||
 | 
			
		||||
    $this->commonPermissions = [
 | 
			
		||||
      'view any unpublished content',
 | 
			
		||||
      "translate {$this->bundle} {$this->entityTypeId}",
 | 
			
		||||
      "create content translations",
 | 
			
		||||
      'use editorial transition create_new_draft',
 | 
			
		||||
      'use editorial transition publish',
 | 
			
		||||
      'use editorial transition archive',
 | 
			
		||||
      'use editorial transition archived_draft',
 | 
			
		||||
      'use editorial transition archived_published',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
 | 
			
		||||
    $entity_type_manager = $this->container->get('entity_type.manager');
 | 
			
		||||
    $this->storage = $entity_type_manager->getStorage($this->entityTypeId);
 | 
			
		||||
 | 
			
		||||
    // @todo Remove this line once https://www.drupal.org/node/2945928 is fixed.
 | 
			
		||||
    $this->config('node.settings')->set('use_admin_theme', '1')->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Enables content moderation for the test entity type and bundle.
 | 
			
		||||
   */
 | 
			
		||||
  protected function enableContentModeration() {
 | 
			
		||||
    $perms = array_merge(parent::getAdministratorPermissions(), [
 | 
			
		||||
      'administer workflows',
 | 
			
		||||
      'view latest version',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->rootUser = $this->drupalCreateUser($perms);
 | 
			
		||||
    $this->drupalLogin($this->rootUser);
 | 
			
		||||
    $workflow_id = 'editorial';
 | 
			
		||||
    $this->drupalGet('/admin/config/workflow/workflows');
 | 
			
		||||
    $edit['bundles[' . $this->bundle . ']'] = TRUE;
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/' . $workflow_id . '/type/' . $this->entityTypeId);
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    // Ensure the parent environment is up-to-date.
 | 
			
		||||
    // @see content_moderation_workflow_insert()
 | 
			
		||||
    \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
 | 
			
		||||
    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
 | 
			
		||||
    /** @var \Drupal\Core\Routing\RouteBuilderInterface $router_builder */
 | 
			
		||||
    $router_builder = $this->container->get('router.builder');
 | 
			
		||||
    $router_builder->rebuildIfNeeded();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getEditorPermissions() {
 | 
			
		||||
    $editor_permissions = [
 | 
			
		||||
      "edit any {$this->bundle} content",
 | 
			
		||||
      "delete any {$this->bundle} content",
 | 
			
		||||
      "view {$this->bundle} revisions",
 | 
			
		||||
      "delete {$this->bundle} revisions",
 | 
			
		||||
    ];
 | 
			
		||||
    return array_merge($editor_permissions, $this->commonPermissions);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getTranslatorPermissions() {
 | 
			
		||||
    return array_merge(parent::getTranslatorPermissions(), $this->commonPermissions);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setupBundle() {
 | 
			
		||||
    parent::setupBundle();
 | 
			
		||||
    $this->createContentType(['type' => $this->bundle]);
 | 
			
		||||
    $this->createEditorialWorkflow();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Loads the active revision translation for the specified entity.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
 | 
			
		||||
   *   The entity being edited.
 | 
			
		||||
   * @param string $langcode
 | 
			
		||||
   *   The translation language code.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Entity\ContentEntityInterface|null
 | 
			
		||||
   *   The active revision translation or NULL if none could be identified.
 | 
			
		||||
   */
 | 
			
		||||
  protected function loadRevisionTranslation(ContentEntityInterface $entity, $langcode) {
 | 
			
		||||
    // Explicitly invalidate the cache for that node, as the call below is
 | 
			
		||||
    // statically cached.
 | 
			
		||||
    $this->storage->resetCache([$entity->id()]);
 | 
			
		||||
    $revision_id = $this->storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode);
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
 | 
			
		||||
    $revision = $revision_id ? $this->storage->loadRevision($revision_id) : NULL;
 | 
			
		||||
    return $revision && $revision->hasTranslation($langcode) ? $revision->getTranslation($langcode) : NULL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the edit URL for the specified entity.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
 | 
			
		||||
   *   The entity being edited.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Url
 | 
			
		||||
   *   The edit URL.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getEditUrl(ContentEntityInterface $entity) {
 | 
			
		||||
    if ($entity->access('update', $this->loggedInUser)) {
 | 
			
		||||
      $url = $entity->toUrl('edit-form');
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $url = $entity->toUrl('drupal:content-translation-edit');
 | 
			
		||||
      $url->setRouteParameter('language', $entity->language()->getId());
 | 
			
		||||
    }
 | 
			
		||||
    return $url;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the delete translation URL for the specified entity.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
 | 
			
		||||
   *   The entity being edited.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Url
 | 
			
		||||
   *   The delete translation URL.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getDeleteUrl(ContentEntityInterface $entity) {
 | 
			
		||||
    if ($entity->access('delete', $this->loggedInUser)) {
 | 
			
		||||
      $url = $entity->toUrl('delete-form');
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $url = $entity->toUrl('drupal:content-translation-delete');
 | 
			
		||||
      $url->setRouteParameter('language', $entity->language()->getId());
 | 
			
		||||
    }
 | 
			
		||||
    return $url;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,227 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests that revision translation deletion is handled correctly.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationRevisionTranslationDeletionTest extends ContentTranslationPendingRevisionTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->doSetup();
 | 
			
		||||
    $this->enableContentModeration();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that translation overview handles pending revisions correctly.
 | 
			
		||||
   */
 | 
			
		||||
  public function testOverview(): void {
 | 
			
		||||
    $index = 1;
 | 
			
		||||
    $accounts = [
 | 
			
		||||
      $this->rootUser,
 | 
			
		||||
      $this->editor,
 | 
			
		||||
      $this->translator,
 | 
			
		||||
    ];
 | 
			
		||||
    foreach ($accounts as $account) {
 | 
			
		||||
      $this->currentAccount = $account;
 | 
			
		||||
      $this->doTestOverview($index++);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Performs a test run.
 | 
			
		||||
   *
 | 
			
		||||
   * @param int $index
 | 
			
		||||
   *   The test run index.
 | 
			
		||||
   */
 | 
			
		||||
  public function doTestOverview($index): void {
 | 
			
		||||
    $this->drupalLogin($this->currentAccount);
 | 
			
		||||
 | 
			
		||||
    // Create a test node.
 | 
			
		||||
    $values = [
 | 
			
		||||
      'title' => "Test $index.1 EN",
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ];
 | 
			
		||||
    $id = $this->createEntity($values, 'en');
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
 | 
			
		||||
    $entity = $this->storage->load($id);
 | 
			
		||||
 | 
			
		||||
    // Add a draft translation and check that it is available only in the latest
 | 
			
		||||
    // revision.
 | 
			
		||||
    $add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add",
 | 
			
		||||
      [
 | 
			
		||||
        $entity->getEntityTypeId() => $id,
 | 
			
		||||
        'source' => 'en',
 | 
			
		||||
        'target' => 'it',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'language' => ConfigurableLanguage::load('it'),
 | 
			
		||||
        'absolute' => FALSE,
 | 
			
		||||
      ]
 | 
			
		||||
    );
 | 
			
		||||
    $add_translation_href = $add_translation_url->toString();
 | 
			
		||||
    $this->drupalGet($add_translation_url);
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => "Test $index.2 IT",
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    $entity = $this->storage->loadUnchanged($id);
 | 
			
		||||
    $this->assertFalse($entity->hasTranslation('it'));
 | 
			
		||||
    $it_revision = $this->loadRevisionTranslation($entity, 'it');
 | 
			
		||||
    $this->assertTrue($it_revision->hasTranslation('it'));
 | 
			
		||||
 | 
			
		||||
    // Check that translations cannot be deleted in drafts.
 | 
			
		||||
    $overview_url = $entity->toUrl('drupal:content-translation-overview');
 | 
			
		||||
    $this->drupalGet($overview_url);
 | 
			
		||||
    $it_delete_url = $this->getDeleteUrl($it_revision);
 | 
			
		||||
    $it_delete_href = $it_delete_url->toString();
 | 
			
		||||
    $this->assertSession()->linkByHrefNotExists($it_delete_href);
 | 
			
		||||
    $warning = 'The "Delete translation" action is only available for published translations.';
 | 
			
		||||
    $this->assertSession()->statusMessageContains($warning, 'warning');
 | 
			
		||||
    $this->drupalGet($this->getEditUrl($it_revision));
 | 
			
		||||
    $this->assertSession()->linkNotExistsExact('Delete translation');
 | 
			
		||||
 | 
			
		||||
    // Publish the translation and verify it can be deleted.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => "Test $index.3 IT",
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    $entity = $this->storage->loadUnchanged($id);
 | 
			
		||||
    $this->assertTrue($entity->hasTranslation('it'));
 | 
			
		||||
    $it_revision = $this->loadRevisionTranslation($entity, 'it');
 | 
			
		||||
    $this->assertTrue($it_revision->hasTranslation('it'));
 | 
			
		||||
    $this->drupalGet($overview_url);
 | 
			
		||||
    $this->assertSession()->linkByHrefExists($it_delete_href);
 | 
			
		||||
    $this->assertSession()->statusMessageNotContains($warning);
 | 
			
		||||
    $this->drupalGet($this->getEditUrl($it_revision));
 | 
			
		||||
    $this->assertSession()->linkExistsExact('Delete translation');
 | 
			
		||||
 | 
			
		||||
    // Create an English draft and verify the published translation was
 | 
			
		||||
    // preserved.
 | 
			
		||||
    $this->drupalLogin($this->editor);
 | 
			
		||||
    $en_revision = $this->loadRevisionTranslation($entity, 'en');
 | 
			
		||||
    $this->drupalGet($this->getEditUrl($en_revision));
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => "Test $index.4 EN",
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    $entity = $this->storage->loadUnchanged($id);
 | 
			
		||||
    $this->assertTrue($entity->hasTranslation('it'));
 | 
			
		||||
    $en_revision = $this->loadRevisionTranslation($entity, 'en');
 | 
			
		||||
    $this->assertTrue($en_revision->hasTranslation('it'));
 | 
			
		||||
    $this->drupalLogin($this->currentAccount);
 | 
			
		||||
 | 
			
		||||
    // Delete the translation and verify that it is actually gone and that it is
 | 
			
		||||
    // possible to create it again.
 | 
			
		||||
    $this->drupalGet($it_delete_url);
 | 
			
		||||
    $this->submitForm([], 'Delete Italian translation');
 | 
			
		||||
    $entity = $this->storage->loadUnchanged($id);
 | 
			
		||||
    $this->assertFalse($entity->hasTranslation('it'));
 | 
			
		||||
    $it_revision = $this->loadRevisionTranslation($entity, 'it');
 | 
			
		||||
    $this->assertTrue($it_revision->wasDefaultRevision());
 | 
			
		||||
    $this->assertTrue($it_revision->hasTranslation('it'));
 | 
			
		||||
    $this->assertLessThan($entity->getRevisionId(), $it_revision->getRevisionId());
 | 
			
		||||
    $this->drupalGet($overview_url);
 | 
			
		||||
    $this->assertSession()->linkByHrefNotExists($this->getEditUrl($it_revision)->toString());
 | 
			
		||||
    $this->assertSession()->linkByHrefExists($add_translation_href);
 | 
			
		||||
 | 
			
		||||
    // Publish the English draft and verify the translation is not accidentally
 | 
			
		||||
    // restored.
 | 
			
		||||
    $this->drupalLogin($this->editor);
 | 
			
		||||
    $en_revision = $this->loadRevisionTranslation($entity, 'en');
 | 
			
		||||
    $this->drupalGet($this->getEditUrl($en_revision));
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => "Test $index.6 EN",
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $entity = $this->storage->loadUnchanged($id);
 | 
			
		||||
    $this->assertFalse($entity->hasTranslation('it'));
 | 
			
		||||
    $this->drupalLogin($this->currentAccount);
 | 
			
		||||
 | 
			
		||||
    // Create a published translation again and verify it could be deleted.
 | 
			
		||||
    $this->drupalGet($add_translation_url);
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => "Test $index.7 IT",
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    $entity = $this->storage->loadUnchanged($id);
 | 
			
		||||
    $this->assertTrue($entity->hasTranslation('it'));
 | 
			
		||||
    $it_revision = $this->loadRevisionTranslation($entity, 'it');
 | 
			
		||||
    $this->assertTrue($it_revision->hasTranslation('it'));
 | 
			
		||||
    $this->drupalGet($overview_url);
 | 
			
		||||
    $this->assertSession()->linkByHrefExists($it_delete_href);
 | 
			
		||||
 | 
			
		||||
    // Create a translation draft again and verify it cannot be deleted.
 | 
			
		||||
    $this->drupalGet($this->getEditUrl($it_revision));
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => "Test $index.8 IT",
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    $entity = $this->storage->loadUnchanged($id);
 | 
			
		||||
    $this->assertTrue($entity->hasTranslation('it'));
 | 
			
		||||
    $it_revision = $this->loadRevisionTranslation($entity, 'it');
 | 
			
		||||
    $this->assertTrue($it_revision->hasTranslation('it'));
 | 
			
		||||
    $this->drupalGet($overview_url);
 | 
			
		||||
    $this->assertSession()->linkByHrefNotExists($it_delete_href);
 | 
			
		||||
 | 
			
		||||
    // Delete the translation draft and verify the translation can be deleted
 | 
			
		||||
    // again, since the active revision is now a default revision.
 | 
			
		||||
    $this->drupalLogin($this->editor);
 | 
			
		||||
    $this->drupalGet($it_revision->toUrl('version-history'));
 | 
			
		||||
    $revision_deletion_url = Url::fromRoute('node.revision_delete_confirm',
 | 
			
		||||
      [
 | 
			
		||||
        'node' => $id,
 | 
			
		||||
        'node_revision' => $it_revision->getRevisionId(),
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'language' => ConfigurableLanguage::load('it'),
 | 
			
		||||
        'absolute' => FALSE,
 | 
			
		||||
      ]
 | 
			
		||||
    );
 | 
			
		||||
    $revision_deletion_href = $revision_deletion_url->toString();
 | 
			
		||||
    $this->getSession()->getDriver()->click("//a[@href='$revision_deletion_href']");
 | 
			
		||||
    $this->submitForm([], 'Delete');
 | 
			
		||||
    $this->drupalLogin($this->currentAccount);
 | 
			
		||||
    $this->drupalGet($overview_url);
 | 
			
		||||
    $this->assertSession()->linkByHrefExists($it_delete_href);
 | 
			
		||||
 | 
			
		||||
    // Verify that now the translation can be deleted.
 | 
			
		||||
    $this->drupalGet($this->getEditUrl($it_revision)->setOption('query', ['destination', '/kittens']));
 | 
			
		||||
    $this->clickLink('Delete translation');
 | 
			
		||||
    $this->submitForm([], 'Delete Italian translation');
 | 
			
		||||
    $this->assertStringEndsWith('/kittens', $this->getSession()->getCurrentUrl());
 | 
			
		||||
 | 
			
		||||
    $entity = $this->storage->loadUnchanged($id);
 | 
			
		||||
    $this->assertFalse($entity->hasTranslation('it'));
 | 
			
		||||
    $it_revision = $this->loadRevisionTranslation($entity, 'it');
 | 
			
		||||
    $this->assertTrue($it_revision->wasDefaultRevision());
 | 
			
		||||
    $this->assertTrue($it_revision->hasTranslation('it'));
 | 
			
		||||
    $this->assertLessThan($entity->getRevisionId(), $it_revision->getRevisionId());
 | 
			
		||||
    $this->drupalGet($overview_url);
 | 
			
		||||
    $this->assertSession()->linkByHrefNotExists($this->getEditUrl($it_revision)->toString());
 | 
			
		||||
    $this->assertSession()->linkByHrefExists($add_translation_href);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,331 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
 | 
			
		||||
use Drupal\comment\Tests\CommentTestTrait;
 | 
			
		||||
use Drupal\Core\Field\Entity\BaseFieldOverride;
 | 
			
		||||
use Drupal\Core\Language\Language;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\language\Entity\ContentLanguageSettings;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the content translation settings UI.
 | 
			
		||||
 *
 | 
			
		||||
 * @covers \Drupal\language\Form\ContentLanguageSettingsForm
 | 
			
		||||
 * @covers ::_content_translation_form_language_content_settings_form_alter
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationSettingsTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use CommentTestTrait;
 | 
			
		||||
  use FieldUiTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'node',
 | 
			
		||||
    'comment',
 | 
			
		||||
    'field_ui',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    // Set up two content types to test fields shared between different
 | 
			
		||||
    // bundles.
 | 
			
		||||
    $this->drupalCreateContentType(['type' => 'article']);
 | 
			
		||||
    $this->drupalCreateContentType(['type' => 'page']);
 | 
			
		||||
    $this->addDefaultCommentField('node', 'article', 'comment_article', CommentItemInterface::OPEN, 'comment_article');
 | 
			
		||||
    $this->addDefaultCommentField('node', 'page', 'comment_page');
 | 
			
		||||
 | 
			
		||||
    $admin_user = $this->drupalCreateUser([
 | 
			
		||||
      'access administration pages',
 | 
			
		||||
      'administer languages',
 | 
			
		||||
      'administer content translation',
 | 
			
		||||
      'administer content types',
 | 
			
		||||
      'administer node fields',
 | 
			
		||||
      'administer comment fields',
 | 
			
		||||
      'administer comments',
 | 
			
		||||
      'administer comment types',
 | 
			
		||||
      'administer account settings',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($admin_user);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that the settings UI works as expected.
 | 
			
		||||
   */
 | 
			
		||||
  public function testSettingsUI(): void {
 | 
			
		||||
    // Check for the content_translation_menu_links_discovered_alter() changes.
 | 
			
		||||
    $this->drupalGet('admin/config');
 | 
			
		||||
    $this->assertSession()->linkExists('Content language and translation');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Configure language and translation support for content.');
 | 
			
		||||
    // Test that the translation settings are ignored if the bundle is marked
 | 
			
		||||
    // translatable but the entity type is not.
 | 
			
		||||
    $edit = ['settings[comment][comment_article][translatable]' => TRUE];
 | 
			
		||||
    $this->assertSettings('comment', 'comment_article', FALSE, $edit);
 | 
			
		||||
 | 
			
		||||
    // Test that the translation settings are ignored if only a field is marked
 | 
			
		||||
    // as translatable and not the related entity type and bundle.
 | 
			
		||||
    $edit = ['settings[comment][comment_article][fields][comment_body]' => TRUE];
 | 
			
		||||
    $this->assertSettings('comment', 'comment_article', FALSE, $edit);
 | 
			
		||||
 | 
			
		||||
    // Test that the translation settings are not stored if an entity type and
 | 
			
		||||
    // bundle are marked as translatable but no field is.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'entity_types[comment]' => TRUE,
 | 
			
		||||
      'settings[comment][comment_article][translatable]' => TRUE,
 | 
			
		||||
      // Base fields are translatable by default.
 | 
			
		||||
      'settings[comment][comment_article][fields][changed]' => FALSE,
 | 
			
		||||
      'settings[comment][comment_article][fields][created]' => FALSE,
 | 
			
		||||
      'settings[comment][comment_article][fields][homepage]' => FALSE,
 | 
			
		||||
      'settings[comment][comment_article][fields][hostname]' => FALSE,
 | 
			
		||||
      'settings[comment][comment_article][fields][mail]' => FALSE,
 | 
			
		||||
      'settings[comment][comment_article][fields][name]' => FALSE,
 | 
			
		||||
      'settings[comment][comment_article][fields][status]' => FALSE,
 | 
			
		||||
      'settings[comment][comment_article][fields][subject]' => FALSE,
 | 
			
		||||
      'settings[comment][comment_article][fields][uid]' => FALSE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertSettings('comment', 'comment_article', FALSE, $edit);
 | 
			
		||||
    $this->assertSession()->statusMessageContains('At least one field needs to be translatable to enable Comment_article for translation.', 'error');
 | 
			
		||||
 | 
			
		||||
    // Test that the translation settings are not stored if a non-configurable
 | 
			
		||||
    // language is set as default and the language selector is hidden.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'entity_types[comment]' => TRUE,
 | 
			
		||||
      'settings[comment][comment_article][settings][language][langcode]' => Language::LANGCODE_NOT_SPECIFIED,
 | 
			
		||||
      'settings[comment][comment_article][settings][language][language_alterable]' => FALSE,
 | 
			
		||||
      'settings[comment][comment_article][translatable]' => TRUE,
 | 
			
		||||
      'settings[comment][comment_article][fields][comment_body]' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertSettings('comment', 'comment_article', FALSE, $edit);
 | 
			
		||||
    $this->assertSession()->statusMessageContains('Translation is not supported if language is always one of: Not specified, Not applicable', 'error');
 | 
			
		||||
 | 
			
		||||
    // Test that a field shared among different bundles can be enabled without
 | 
			
		||||
    // needing to make all the related bundles translatable.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'entity_types[comment]' => TRUE,
 | 
			
		||||
      'settings[comment][comment_article][settings][language][langcode]' => 'current_interface',
 | 
			
		||||
      'settings[comment][comment_article][settings][language][language_alterable]' => TRUE,
 | 
			
		||||
      'settings[comment][comment_article][translatable]' => TRUE,
 | 
			
		||||
      'settings[comment][comment_article][fields][comment_body]' => TRUE,
 | 
			
		||||
      // Override both comment subject fields to untranslatable.
 | 
			
		||||
      'settings[comment][comment_article][fields][subject]' => FALSE,
 | 
			
		||||
      'settings[comment][comment][fields][subject]' => FALSE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertSettings('comment', 'comment_article', TRUE, $edit);
 | 
			
		||||
    $entity_field_manager = \Drupal::service('entity_field.manager');
 | 
			
		||||
    $definition = $entity_field_manager->getFieldDefinitions('comment', 'comment_article')['comment_body'];
 | 
			
		||||
    $this->assertTrue($definition->isTranslatable(), 'Article comment body is translatable.');
 | 
			
		||||
    $definition = $entity_field_manager->getFieldDefinitions('comment', 'comment_article')['subject'];
 | 
			
		||||
    $this->assertFalse($definition->isTranslatable(), 'Article comment subject is not translatable.');
 | 
			
		||||
 | 
			
		||||
    $definition = $entity_field_manager->getFieldDefinitions('comment', 'comment')['comment_body'];
 | 
			
		||||
    $this->assertFalse($definition->isTranslatable(), 'Page comment body is not translatable.');
 | 
			
		||||
    $definition = $entity_field_manager->getFieldDefinitions('comment', 'comment')['subject'];
 | 
			
		||||
    $this->assertFalse($definition->isTranslatable(), 'Page comment subject is not translatable.');
 | 
			
		||||
 | 
			
		||||
    // Test that translation can be enabled for base fields.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'entity_types[entity_test_mul]' => TRUE,
 | 
			
		||||
      'settings[entity_test_mul][entity_test_mul][translatable]' => TRUE,
 | 
			
		||||
      'settings[entity_test_mul][entity_test_mul][fields][name]' => TRUE,
 | 
			
		||||
      'settings[entity_test_mul][entity_test_mul][fields][user_id]' => FALSE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertSettings('entity_test_mul', 'entity_test_mul', TRUE, $edit);
 | 
			
		||||
    $field_override = BaseFieldOverride::loadByName('entity_test_mul', 'entity_test_mul', 'name');
 | 
			
		||||
    $this->assertTrue($field_override->isTranslatable(), 'Base fields can be overridden with a base field bundle override entity.');
 | 
			
		||||
    $definitions = $entity_field_manager->getFieldDefinitions('entity_test_mul', 'entity_test_mul');
 | 
			
		||||
    $this->assertTrue($definitions['name']->isTranslatable());
 | 
			
		||||
    $this->assertFalse($definitions['user_id']->isTranslatable());
 | 
			
		||||
 | 
			
		||||
    // Test that language settings are correctly stored.
 | 
			
		||||
    $language_configuration = ContentLanguageSettings::loadByEntityTypeBundle('comment', 'comment_article');
 | 
			
		||||
    $this->assertEquals('current_interface', $language_configuration->getDefaultLangcode(), 'The default language for article comments is set to the interface text language selected for page.');
 | 
			
		||||
    $this->assertTrue($language_configuration->isLanguageAlterable(), 'The language selector for article comments is shown.');
 | 
			
		||||
 | 
			
		||||
    // Verify language widget appears on comment type form.
 | 
			
		||||
    $this->drupalGet('admin/structure/comment/manage/comment_article');
 | 
			
		||||
    $this->assertSession()->fieldExists('language_configuration[content_translation]');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-language-configuration-content-translation');
 | 
			
		||||
 | 
			
		||||
    // Verify that translation may be enabled for the article content type.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'language_configuration[content_translation]' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    // Make sure the checkbox is available and not checked by default.
 | 
			
		||||
    $this->drupalGet('admin/structure/types/manage/article');
 | 
			
		||||
    $this->assertSession()->fieldExists('language_configuration[content_translation]');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-language-configuration-content-translation');
 | 
			
		||||
    $this->drupalGet('admin/structure/types/manage/article');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->drupalGet('admin/structure/types/manage/article');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-language-configuration-content-translation');
 | 
			
		||||
 | 
			
		||||
    // Test that the title field of nodes is available in the settings form.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'entity_types[node]' => TRUE,
 | 
			
		||||
      'settings[node][article][settings][language][langcode]' => 'current_interface',
 | 
			
		||||
      'settings[node][article][settings][language][language_alterable]' => TRUE,
 | 
			
		||||
      'settings[node][article][translatable]' => TRUE,
 | 
			
		||||
      'settings[node][article][fields][title]' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertSettings('node', 'article', TRUE, $edit);
 | 
			
		||||
 | 
			
		||||
    foreach ([TRUE, FALSE] as $translatable) {
 | 
			
		||||
      // Test that configurable field translatability is correctly switched.
 | 
			
		||||
      $edit = ['settings[node][article][fields][body]' => $translatable];
 | 
			
		||||
      $this->assertSettings('node', 'article', TRUE, $edit);
 | 
			
		||||
      $field = FieldConfig::loadByName('node', 'article', 'body');
 | 
			
		||||
      $definitions = $entity_field_manager->getFieldDefinitions('node', 'article');
 | 
			
		||||
      $this->assertEquals($translatable, $definitions['body']->isTranslatable(), 'Field translatability correctly switched.');
 | 
			
		||||
      $this->assertEquals($definitions['body']->isTranslatable(), $field->isTranslatable(), 'Configurable field translatability correctly switched.');
 | 
			
		||||
 | 
			
		||||
      // Test that also the Field UI form behaves correctly.
 | 
			
		||||
      $translatable = !$translatable;
 | 
			
		||||
      $edit = ['translatable' => $translatable];
 | 
			
		||||
      $this->drupalGet('admin/structure/types/manage/article/fields/node.article.body');
 | 
			
		||||
      $this->submitForm($edit, 'Save settings');
 | 
			
		||||
      $entity_field_manager->clearCachedFieldDefinitions();
 | 
			
		||||
      $field = FieldConfig::loadByName('node', 'article', 'body');
 | 
			
		||||
      $definitions = $entity_field_manager->getFieldDefinitions('node', 'article');
 | 
			
		||||
      $this->assertEquals($translatable, $definitions['body']->isTranslatable(), 'Field translatability correctly switched.');
 | 
			
		||||
      $this->assertEquals($definitions['body']->isTranslatable(), $field->isTranslatable(), 'Configurable field translatability correctly switched.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Test that we can't use the 'Not specified' default language when it is
 | 
			
		||||
    // not showing in the language selector.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'language_configuration[langcode]' => 'und',
 | 
			
		||||
      'language_configuration[language_alterable]' => FALSE,
 | 
			
		||||
      'language_configuration[content_translation]' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('admin/structure/types/manage/article');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->getSession()->getPage()->hasContent('"Show language selector" is not compatible with translating content that has default language: und. Either do not hide the language selector or pick a specific language.');
 | 
			
		||||
 | 
			
		||||
    // Test that the order of the language list is similar to other language
 | 
			
		||||
    // lists, such as in Views UI.
 | 
			
		||||
    $this->drupalGet('admin/config/regional/content-language');
 | 
			
		||||
 | 
			
		||||
    $expected_elements = [
 | 
			
		||||
      'site_default',
 | 
			
		||||
      'current_interface',
 | 
			
		||||
      'authors_default',
 | 
			
		||||
      'en',
 | 
			
		||||
      'und',
 | 
			
		||||
      'zxx',
 | 
			
		||||
    ];
 | 
			
		||||
    $options = $this->assertSession()->selectExists('edit-settings-node-article-settings-language-langcode')->findAll('css', 'option');
 | 
			
		||||
    $options = array_map(function ($item) {
 | 
			
		||||
      return $item->getValue();
 | 
			
		||||
    }, $options);
 | 
			
		||||
    $this->assertSame($expected_elements, $options);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the language settings checkbox on account settings page.
 | 
			
		||||
   */
 | 
			
		||||
  public function testAccountLanguageSettingsUI(): void {
 | 
			
		||||
    // Make sure the checkbox is available and not checked by default.
 | 
			
		||||
    $this->drupalGet('admin/config/people/accounts');
 | 
			
		||||
    $this->assertSession()->fieldExists('language[content_translation]');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-language-content-translation');
 | 
			
		||||
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'language[content_translation]' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('admin/config/people/accounts');
 | 
			
		||||
    $this->submitForm($edit, 'Save configuration');
 | 
			
		||||
    $this->drupalGet('admin/config/people/accounts');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-language-content-translation');
 | 
			
		||||
 | 
			
		||||
    // Make sure account settings can be saved.
 | 
			
		||||
    $this->drupalGet('admin/config/people/accounts');
 | 
			
		||||
    $this->submitForm(['anonymous' => 'Save me!'], 'Save configuration');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('anonymous', 'Save me!');
 | 
			
		||||
    $this->assertSession()->statusMessageContains('The configuration options have been saved.', 'status');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts that translatability has the expected value for the given bundle.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $entity_type
 | 
			
		||||
   *   The entity type for which to check translatability.
 | 
			
		||||
   * @param string|null $bundle
 | 
			
		||||
   *   The bundle for which to check translatability.
 | 
			
		||||
   * @param bool $enabled
 | 
			
		||||
   *   TRUE if translatability should be enabled, FALSE otherwise.
 | 
			
		||||
   * @param array $edit
 | 
			
		||||
   *   An array of values to submit to the content translation settings page.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertSettings(string $entity_type, ?string $bundle, bool $enabled, array $edit): void {
 | 
			
		||||
    $this->drupalGet('admin/config/regional/content-language');
 | 
			
		||||
    $this->submitForm($edit, 'Save configuration');
 | 
			
		||||
    $status = $enabled ? 'enabled' : 'disabled';
 | 
			
		||||
    $message = "Translation for entity $entity_type ($bundle) is $status.";
 | 
			
		||||
    $this->assertEquals($enabled, \Drupal::service('content_translation.manager')->isEnabled($entity_type, $bundle), $message);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that field setting depends on bundle translatability.
 | 
			
		||||
   */
 | 
			
		||||
  public function testFieldTranslatableSettingsUI(): void {
 | 
			
		||||
    // At least one field needs to be translatable to enable article for
 | 
			
		||||
    // translation. Create an extra field to be used for this purpose. We use
 | 
			
		||||
    // the UI to test our form alterations.
 | 
			
		||||
    $this->fieldUIAddNewField('admin/structure/types/manage/article', 'article_text', 'Test', 'text');
 | 
			
		||||
 | 
			
		||||
    // Tests that field doesn't have translatable setting if bundle is not
 | 
			
		||||
    // translatable.
 | 
			
		||||
    $path = 'admin/structure/types/manage/article/fields/node.article.field_article_text';
 | 
			
		||||
    $this->drupalGet($path);
 | 
			
		||||
    $this->assertSession()->fieldDisabled('edit-translatable');
 | 
			
		||||
    $this->assertSession()->pageTextContains('To configure translation for this field, enable language support for this type.');
 | 
			
		||||
 | 
			
		||||
    // 'Users may translate this field' should be unchecked by default.
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('translatable');
 | 
			
		||||
 | 
			
		||||
    // Tests that field has translatable setting if bundle is translatable.
 | 
			
		||||
    // Note: this field is not translatable when enable bundle translatability.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'entity_types[node]' => TRUE,
 | 
			
		||||
      'settings[node][article][settings][language][language_alterable]' => TRUE,
 | 
			
		||||
      'settings[node][article][translatable]' => TRUE,
 | 
			
		||||
      'settings[node][article][fields][field_article_text]' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertSettings('node', 'article', TRUE, $edit);
 | 
			
		||||
    $this->drupalGet($path);
 | 
			
		||||
    $this->assertSession()->fieldEnabled('edit-translatable');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-translatable');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('To enable translation of this field, enable language support for this type.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the translatable settings checkbox for untranslatable entities.
 | 
			
		||||
   */
 | 
			
		||||
  public function testNonTranslatableTranslationSettingsUI(): void {
 | 
			
		||||
    $this->drupalGet('admin/config/regional/content-language');
 | 
			
		||||
    $this->assertSession()->fieldNotExists('settings[entity_test][entity_test][translatable]');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,174 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\block_content\Entity\BlockContentType;
 | 
			
		||||
use Drupal\comment\Entity\CommentType;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the Content translation settings.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationStandardFieldsTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'node',
 | 
			
		||||
    'comment',
 | 
			
		||||
    'field_ui',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $admin_user = $this->drupalCreateUser([
 | 
			
		||||
      'access administration pages',
 | 
			
		||||
      'administer languages',
 | 
			
		||||
      'administer content translation',
 | 
			
		||||
      'administer content types',
 | 
			
		||||
      'administer node fields',
 | 
			
		||||
      'administer comment fields',
 | 
			
		||||
      'administer comments',
 | 
			
		||||
      'administer comment types',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($admin_user);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that translatable fields are being rendered.
 | 
			
		||||
   */
 | 
			
		||||
  public function testFieldTranslatableArticle(): void {
 | 
			
		||||
    // Install block and field modules.
 | 
			
		||||
    \Drupal::service('module_installer')->install(
 | 
			
		||||
      [
 | 
			
		||||
        'block',
 | 
			
		||||
        'block_content',
 | 
			
		||||
        'filter',
 | 
			
		||||
        'image',
 | 
			
		||||
        'text',
 | 
			
		||||
      ]);
 | 
			
		||||
 | 
			
		||||
    // Create a basic block type with a body field.
 | 
			
		||||
    $bundle = BlockContentType::create([
 | 
			
		||||
      'id' => 'basic',
 | 
			
		||||
      'label' => 'Basic',
 | 
			
		||||
      'revision' => FALSE,
 | 
			
		||||
    ]);
 | 
			
		||||
    $bundle->save();
 | 
			
		||||
    block_content_add_body_field($bundle->id());
 | 
			
		||||
 | 
			
		||||
    // Create a comment type with a body field.
 | 
			
		||||
    $bundle = CommentType::create([
 | 
			
		||||
      'id' => 'comment',
 | 
			
		||||
      'label' => 'Comment',
 | 
			
		||||
      'target_entity_type_id' => 'node',
 | 
			
		||||
    ]);
 | 
			
		||||
    $bundle->save();
 | 
			
		||||
    \Drupal::service('comment.manager')->addBodyField('comment');
 | 
			
		||||
 | 
			
		||||
    // Create the article content type and add a comment, image and tag field.
 | 
			
		||||
    $this->drupalCreateContentType(['type' => 'article', 'title' => 'Article']);
 | 
			
		||||
 | 
			
		||||
    $entity_type_manager = \Drupal::entityTypeManager();
 | 
			
		||||
    $entity_type_manager->getStorage('field_storage_config')->create([
 | 
			
		||||
      'entity_type' => 'node',
 | 
			
		||||
      'field_name' => 'comment',
 | 
			
		||||
      'type' => 'text',
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    $entity_type_manager->getStorage('field_config')->create([
 | 
			
		||||
      'label' => 'Comments',
 | 
			
		||||
      'field_name' => 'comment',
 | 
			
		||||
      'entity_type' => 'node',
 | 
			
		||||
      'bundle' => 'article',
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    $entity_type_manager->getStorage('field_storage_config')->create([
 | 
			
		||||
      'entity_type' => 'node',
 | 
			
		||||
      'field_name' => 'field_image',
 | 
			
		||||
      'type' => 'image',
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    $entity_type_manager->getStorage('field_config')->create([
 | 
			
		||||
      'label' => 'Image',
 | 
			
		||||
      'field_name' => 'field_image',
 | 
			
		||||
      'entity_type' => 'node',
 | 
			
		||||
      'bundle' => 'article',
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    $entity_type_manager->getStorage('field_storage_config')->create([
 | 
			
		||||
      'entity_type' => 'node',
 | 
			
		||||
      'field_name' => 'field_tags',
 | 
			
		||||
      'type' => 'text',
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    $entity_type_manager->getStorage('field_config')->create([
 | 
			
		||||
      'label' => 'Tags',
 | 
			
		||||
      'field_name' => 'field_tags',
 | 
			
		||||
      'entity_type' => 'node',
 | 
			
		||||
      'bundle' => 'article',
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    $entity_type_manager->getStorage('field_storage_config')->create([
 | 
			
		||||
      'entity_type' => 'user',
 | 
			
		||||
      'field_name' => 'user_picture',
 | 
			
		||||
      'type' => 'image',
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    // Add a user picture field to the user entity.
 | 
			
		||||
    $entity_type_manager->getStorage('field_config')->create([
 | 
			
		||||
      'label' => 'Tags',
 | 
			
		||||
      'field_name' => 'user_picture',
 | 
			
		||||
      'entity_type' => 'user',
 | 
			
		||||
      'bundle' => 'user',
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    $path = 'admin/config/regional/content-language';
 | 
			
		||||
    $this->drupalGet($path);
 | 
			
		||||
 | 
			
		||||
    // Check content block fields.
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-settings-block-content-basic-fields-body');
 | 
			
		||||
 | 
			
		||||
    // Check comment fields.
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-settings-comment-comment-fields-comment-body');
 | 
			
		||||
 | 
			
		||||
    // Check node fields.
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-settings-node-article-fields-comment');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-settings-node-article-fields-field-image');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-settings-node-article-fields-field-tags');
 | 
			
		||||
 | 
			
		||||
    // Check user fields.
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-settings-user-user-fields-user-picture');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that revision_log is not translatable.
 | 
			
		||||
   */
 | 
			
		||||
  public function testRevisionLogNotTranslatable(): void {
 | 
			
		||||
    $path = 'admin/config/regional/content-language';
 | 
			
		||||
    $this->drupalGet($path);
 | 
			
		||||
    $this->assertSession()->fieldNotExists('edit-settings-node-article-fields-revision-log');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,271 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\EntityInterface;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\file\Entity\File;
 | 
			
		||||
use Drupal\Tests\TestFileCreationTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the field synchronization behavior for the image field.
 | 
			
		||||
 *
 | 
			
		||||
 * @covers ::_content_translation_form_language_content_settings_form_alter
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationSyncImageTest extends ContentTranslationTestBase {
 | 
			
		||||
 | 
			
		||||
  use TestFileCreationTrait {
 | 
			
		||||
    getTestFiles as drupalGetTestFiles;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The cardinality of the image field.
 | 
			
		||||
   *
 | 
			
		||||
   * @var int
 | 
			
		||||
   */
 | 
			
		||||
  protected $cardinality;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The test image files.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $files;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'image',
 | 
			
		||||
    'field_ui',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->doSetup();
 | 
			
		||||
    $this->files = $this->drupalGetTestFiles('image');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates the test image field.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setupTestFields(): void {
 | 
			
		||||
    $this->fieldName = 'field_test_et_ui_image';
 | 
			
		||||
    $this->cardinality = 3;
 | 
			
		||||
 | 
			
		||||
    FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $this->fieldName,
 | 
			
		||||
      'entity_type' => $this->entityTypeId,
 | 
			
		||||
      'type' => 'image',
 | 
			
		||||
      'cardinality' => $this->cardinality,
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'entity_type' => $this->entityTypeId,
 | 
			
		||||
      'field_name' => $this->fieldName,
 | 
			
		||||
      'bundle' => $this->entityTypeId,
 | 
			
		||||
      'label' => 'Test translatable image field',
 | 
			
		||||
      'third_party_settings' => [
 | 
			
		||||
        'content_translation' => [
 | 
			
		||||
          'translation_sync' => [
 | 
			
		||||
            'file' => FALSE,
 | 
			
		||||
            'alt' => 'alt',
 | 
			
		||||
            'title' => 'title',
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ])->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getEditorPermissions(): array {
 | 
			
		||||
    // Every entity-type-specific test needs to define these.
 | 
			
		||||
    return ['administer entity_test_mul fields', 'administer languages', 'administer content translation'];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests image field synchronization.
 | 
			
		||||
   */
 | 
			
		||||
  public function testImageFieldSync(): void {
 | 
			
		||||
    // Check that the alt and title fields are enabled for the image field.
 | 
			
		||||
    $this->drupalLogin($this->editor);
 | 
			
		||||
    $this->drupalGet('entity_test_mul/structure/' . $this->entityTypeId . '/fields/' . $this->entityTypeId . '.' . $this->entityTypeId . '.' . $this->fieldName);
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-third-party-settings-content-translation-translation-sync-alt');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-third-party-settings-content-translation-translation-sync-title');
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'third_party_settings[content_translation][translation_sync][alt]' => FALSE,
 | 
			
		||||
      'third_party_settings[content_translation][translation_sync][title]' => FALSE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save settings');
 | 
			
		||||
 | 
			
		||||
    // Check that the content translation settings page reflects the changes
 | 
			
		||||
    // performed in the field edit page.
 | 
			
		||||
    $this->drupalGet('admin/config/regional/content-language');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-settings-entity-test-mul-entity-test-mul-columns-field-test-et-ui-image-alt');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-settings-entity-test-mul-entity-test-mul-columns-field-test-et-ui-image-title');
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'settings[entity_test_mul][entity_test_mul][fields][field_test_et_ui_image]' => TRUE,
 | 
			
		||||
      'settings[entity_test_mul][entity_test_mul][columns][field_test_et_ui_image][alt]' => TRUE,
 | 
			
		||||
      'settings[entity_test_mul][entity_test_mul][columns][field_test_et_ui_image][title]' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('admin/config/regional/content-language');
 | 
			
		||||
    $this->submitForm($edit, 'Save configuration');
 | 
			
		||||
    $this->assertSession()->statusMessageNotExists('error');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-settings-entity-test-mul-entity-test-mul-columns-field-test-et-ui-image-alt');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-settings-entity-test-mul-entity-test-mul-columns-field-test-et-ui-image-title');
 | 
			
		||||
    $this->drupalLogin($this->translator);
 | 
			
		||||
 | 
			
		||||
    $default_langcode = $this->langcodes[0];
 | 
			
		||||
    $langcode = $this->langcodes[1];
 | 
			
		||||
 | 
			
		||||
    // Populate the test entity with some random initial values.
 | 
			
		||||
    $values = [
 | 
			
		||||
      'name' => $this->randomMachineName(),
 | 
			
		||||
      'user_id' => 2,
 | 
			
		||||
      'langcode' => $default_langcode,
 | 
			
		||||
    ];
 | 
			
		||||
    $entity = \Drupal::entityTypeManager()
 | 
			
		||||
      ->getStorage($this->entityTypeId)
 | 
			
		||||
      ->create($values);
 | 
			
		||||
 | 
			
		||||
    // Create some file entities from the generated test files and store them.
 | 
			
		||||
    $values = [];
 | 
			
		||||
    for ($delta = 0; $delta < $this->cardinality; $delta++) {
 | 
			
		||||
      // For the default language use the same order for files and field items.
 | 
			
		||||
      $index = $delta;
 | 
			
		||||
 | 
			
		||||
      // Create the file entity for the image being processed and record its
 | 
			
		||||
      // identifier.
 | 
			
		||||
      $field_values = [
 | 
			
		||||
        'uri' => $this->files[$index]->uri,
 | 
			
		||||
        'uid' => \Drupal::currentUser()->id(),
 | 
			
		||||
      ];
 | 
			
		||||
      $file = File::create($field_values);
 | 
			
		||||
      $file->setPermanent();
 | 
			
		||||
      $file->save();
 | 
			
		||||
      $fid = $file->id();
 | 
			
		||||
      $this->files[$index]->fid = $fid;
 | 
			
		||||
 | 
			
		||||
      // Generate the item for the current image file entity and attach it to
 | 
			
		||||
      // the entity.
 | 
			
		||||
      $item = [
 | 
			
		||||
        'target_id' => $fid,
 | 
			
		||||
        'alt' => $default_langcode . '_' . $fid . '_' . $this->randomMachineName(),
 | 
			
		||||
        'title' => $default_langcode . '_' . $fid . '_' . $this->randomMachineName(),
 | 
			
		||||
      ];
 | 
			
		||||
      $entity->{$this->fieldName}[] = $item;
 | 
			
		||||
 | 
			
		||||
      // Store the generated values keying them by fid for easier lookup.
 | 
			
		||||
      $values[$default_langcode][$fid] = $item;
 | 
			
		||||
    }
 | 
			
		||||
    $entity = $this->saveEntity($entity);
 | 
			
		||||
 | 
			
		||||
    // Create some field translations for the test image field. The translated
 | 
			
		||||
    // items will be one less than the original values to check that only the
 | 
			
		||||
    // translated ones will be preserved. In fact we want the same fids and
 | 
			
		||||
    // items order for both languages.
 | 
			
		||||
    $translation = $entity->addTranslation($langcode);
 | 
			
		||||
    for ($delta = 0; $delta < $this->cardinality - 1; $delta++) {
 | 
			
		||||
      // Simulate a field reordering: items are shifted of one position ahead.
 | 
			
		||||
      // The modulo operator ensures we start from the beginning after reaching
 | 
			
		||||
      // the maximum allowed delta.
 | 
			
		||||
      $index = ($delta + 1) % $this->cardinality;
 | 
			
		||||
 | 
			
		||||
      // Generate the item for the current image file entity and attach it to
 | 
			
		||||
      // the entity.
 | 
			
		||||
      $fid = $this->files[$index]->fid;
 | 
			
		||||
      $item = [
 | 
			
		||||
        'target_id' => $fid,
 | 
			
		||||
        'alt' => $langcode . '_' . $fid . '_' . $this->randomMachineName(),
 | 
			
		||||
        'title' => $langcode . '_' . $fid . '_' . $this->randomMachineName(),
 | 
			
		||||
      ];
 | 
			
		||||
      $translation->{$this->fieldName}[] = $item;
 | 
			
		||||
 | 
			
		||||
      // Again store the generated values keying them by fid for easier lookup.
 | 
			
		||||
      $values[$langcode][$fid] = $item;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Perform synchronization: the translation language is used as source,
 | 
			
		||||
    // while the default language is used as target.
 | 
			
		||||
    $this->manager->getTranslationMetadata($translation)->setSource($default_langcode);
 | 
			
		||||
    $entity = $this->saveEntity($translation);
 | 
			
		||||
    $translation = $entity->getTranslation($langcode);
 | 
			
		||||
 | 
			
		||||
    // Check that one value has been dropped from the original values.
 | 
			
		||||
    $assert = count($entity->{$this->fieldName}) == 2;
 | 
			
		||||
    $this->assertTrue($assert, 'One item correctly removed from the synchronized field values.');
 | 
			
		||||
 | 
			
		||||
    // Check that fids have been synchronized and translatable column values
 | 
			
		||||
    // have been retained.
 | 
			
		||||
    $fids = [];
 | 
			
		||||
    foreach ($entity->{$this->fieldName} as $delta => $item) {
 | 
			
		||||
      $value = $values[$default_langcode][$item->target_id];
 | 
			
		||||
      $source_item = $translation->{$this->fieldName}->get($delta);
 | 
			
		||||
      $assert = $item->target_id == $source_item->target_id && $item->alt == $value['alt'] && $item->title == $value['title'];
 | 
			
		||||
      $this->assertTrue($assert, "Field item $item->target_id has been successfully synchronized.");
 | 
			
		||||
      $fids[$item->target_id] = TRUE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check that the dropped value is the right one.
 | 
			
		||||
    $removed_fid = $this->files[0]->fid;
 | 
			
		||||
    $this->assertTrue(!isset($fids[$removed_fid]), "Field item $removed_fid has been correctly removed.");
 | 
			
		||||
 | 
			
		||||
    // Add back an item for the dropped value and perform synchronization again.
 | 
			
		||||
    $values[$langcode][$removed_fid] = [
 | 
			
		||||
      'target_id' => $removed_fid,
 | 
			
		||||
      'alt' => $langcode . '_' . $removed_fid . '_' . $this->randomMachineName(),
 | 
			
		||||
      'title' => $langcode . '_' . $removed_fid . '_' . $this->randomMachineName(),
 | 
			
		||||
    ];
 | 
			
		||||
    $translation->{$this->fieldName}->setValue(array_values($values[$langcode]));
 | 
			
		||||
    $entity = $this->saveEntity($translation);
 | 
			
		||||
    $translation = $entity->getTranslation($langcode);
 | 
			
		||||
 | 
			
		||||
    // Check that the value has been added to the default language.
 | 
			
		||||
    $assert = count($entity->{$this->fieldName}->getValue()) == 3;
 | 
			
		||||
    $this->assertTrue($assert, 'One item correctly added to the synchronized field values.');
 | 
			
		||||
 | 
			
		||||
    foreach ($entity->{$this->fieldName} as $delta => $item) {
 | 
			
		||||
      // When adding an item its value is copied over all the target languages,
 | 
			
		||||
      // thus in this case the source language needs to be used to check the
 | 
			
		||||
      // values instead of the target one.
 | 
			
		||||
      $fid_langcode = $item->target_id != $removed_fid ? $default_langcode : $langcode;
 | 
			
		||||
      $value = $values[$fid_langcode][$item->target_id];
 | 
			
		||||
      $source_item = $translation->{$this->fieldName}->get($delta);
 | 
			
		||||
      $assert = $item->target_id == $source_item->target_id && $item->alt == $value['alt'] && $item->title == $value['title'];
 | 
			
		||||
      $this->assertTrue($assert, "Field item $item->target_id has been successfully synchronized.");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Saves the passed entity and reloads it, enabling compatibility mode.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Entity\EntityInterface $entity
 | 
			
		||||
   *   The entity to be saved.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Entity\EntityInterface
 | 
			
		||||
   *   The saved entity.
 | 
			
		||||
   */
 | 
			
		||||
  protected function saveEntity(EntityInterface $entity): EntityInterface {
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $entity = \Drupal::entityTypeManager()->getStorage('entity_test_mul')->loadUnchanged($entity->id());
 | 
			
		||||
    return $entity;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,259 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\ContentEntityInterface;
 | 
			
		||||
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base class for content translation tests.
 | 
			
		||||
 */
 | 
			
		||||
abstract class ContentTranslationTestBase extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['text'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The entity type being tested.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityTypeId = 'entity_test_mul';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The bundle being tested.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $bundle;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The added languages.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $langcodes;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The account to be used to test translation operations.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\user\UserInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $translator;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The account to be used to test multilingual entity editing.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\user\UserInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $editor;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The account to be used to test access to both workflows.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\user\UserInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $administrator;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The name of the field used to test translation.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldName;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The translation controller for the current entity type.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\content_translation\ContentTranslationHandlerInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $controller;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @var \Drupal\content_translation\ContentTranslationManagerInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $manager;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Completes preparation for content translation tests.
 | 
			
		||||
   */
 | 
			
		||||
  protected function doSetup(): void {
 | 
			
		||||
    $this->setupLanguages();
 | 
			
		||||
    $this->setupBundle();
 | 
			
		||||
    $this->enableTranslation();
 | 
			
		||||
    $this->setupUsers();
 | 
			
		||||
    $this->setupTestFields();
 | 
			
		||||
 | 
			
		||||
    $this->manager = $this->container->get('content_translation.manager');
 | 
			
		||||
    $this->controller = $this->manager->getTranslationHandler($this->entityTypeId);
 | 
			
		||||
 | 
			
		||||
    // Rebuild the container so that the new languages are picked up by services
 | 
			
		||||
    // that hold a list of languages.
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Adds additional languages.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setupLanguages() {
 | 
			
		||||
    $this->langcodes = ['it', 'fr'];
 | 
			
		||||
    foreach ($this->langcodes as $langcode) {
 | 
			
		||||
      ConfigurableLanguage::createFromLangcode($langcode)->save();
 | 
			
		||||
    }
 | 
			
		||||
    array_unshift($this->langcodes, \Drupal::languageManager()->getDefaultLanguage()->getId());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns an array of permissions needed for the translator.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getTranslatorPermissions() {
 | 
			
		||||
    return array_filter([$this->getTranslatePermission(), 'create content translations', 'update content translations', 'delete content translations']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the translate permissions for the current entity and bundle.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getTranslatePermission() {
 | 
			
		||||
    $entity_type = \Drupal::entityTypeManager()->getDefinition($this->entityTypeId);
 | 
			
		||||
    if ($permission_granularity = $entity_type->getPermissionGranularity()) {
 | 
			
		||||
      return $permission_granularity == 'bundle' ? "translate {$this->bundle} {$this->entityTypeId}" : "translate {$this->entityTypeId}";
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns an array of permissions needed for the editor.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getEditorPermissions() {
 | 
			
		||||
    // Every entity-type-specific test needs to define these.
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns an array of permissions needed for the administrator.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getAdministratorPermissions() {
 | 
			
		||||
    return array_merge($this->getEditorPermissions(), $this->getTranslatorPermissions(), ['administer languages', 'administer content translation']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates and activates translator, editor and admin users.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setupUsers() {
 | 
			
		||||
    $this->translator = $this->drupalCreateUser($this->getTranslatorPermissions(), 'translator');
 | 
			
		||||
    $this->editor = $this->drupalCreateUser($this->getEditorPermissions(), 'editor');
 | 
			
		||||
    $this->administrator = $this->drupalCreateUser($this->getAdministratorPermissions(), 'administrator');
 | 
			
		||||
    $this->drupalLogin($this->translator);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates or initializes the bundle date if needed.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setupBundle() {
 | 
			
		||||
    if (empty($this->bundle)) {
 | 
			
		||||
      $this->bundle = $this->entityTypeId;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Enables translation for the current entity type and bundle.
 | 
			
		||||
   */
 | 
			
		||||
  protected function enableTranslation() {
 | 
			
		||||
    // Enable translation for the current entity type and ensure the change is
 | 
			
		||||
    // picked up.
 | 
			
		||||
    \Drupal::service('content_translation.manager')->setEnabled($this->entityTypeId, $this->bundle, TRUE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates the test fields.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setupTestFields() {
 | 
			
		||||
    if (empty($this->fieldName)) {
 | 
			
		||||
      $this->fieldName = 'field_test_et_ui_test';
 | 
			
		||||
    }
 | 
			
		||||
    FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $this->fieldName,
 | 
			
		||||
      'type' => 'string',
 | 
			
		||||
      'entity_type' => $this->entityTypeId,
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'entity_type' => $this->entityTypeId,
 | 
			
		||||
      'field_name' => $this->fieldName,
 | 
			
		||||
      'bundle' => $this->bundle,
 | 
			
		||||
      'label' => 'Test translatable text-field',
 | 
			
		||||
    ])->save();
 | 
			
		||||
    /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
 | 
			
		||||
    $display_repository = \Drupal::service('entity_display.repository');
 | 
			
		||||
    $display_repository->getFormDisplay($this->entityTypeId, $this->bundle, 'default')
 | 
			
		||||
      ->setComponent($this->fieldName, [
 | 
			
		||||
        'type' => 'string_textfield',
 | 
			
		||||
        'weight' => 0,
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates the entity to be translated.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $values
 | 
			
		||||
   *   An array of initial values for the entity.
 | 
			
		||||
   * @param string $langcode
 | 
			
		||||
   *   The initial language code of the entity.
 | 
			
		||||
   * @param string $bundle_name
 | 
			
		||||
   *   (optional) The entity bundle, if the entity uses bundles. Defaults to
 | 
			
		||||
   *   NULL. If left NULL, $this->bundle will be used.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   The entity id.
 | 
			
		||||
   */
 | 
			
		||||
  protected function createEntity($values, $langcode, $bundle_name = NULL) {
 | 
			
		||||
    $entity_values = $values;
 | 
			
		||||
    $entity_values['langcode'] = $langcode;
 | 
			
		||||
    $entity_type = \Drupal::entityTypeManager()->getDefinition($this->entityTypeId);
 | 
			
		||||
    if ($bundle_key = $entity_type->getKey('bundle')) {
 | 
			
		||||
      $entity_values[$bundle_key] = $bundle_name ?: $this->bundle;
 | 
			
		||||
    }
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')->getStorage($this->entityTypeId);
 | 
			
		||||
    if (!($storage instanceof SqlContentEntityStorage)) {
 | 
			
		||||
      foreach ($values as $property => $value) {
 | 
			
		||||
        if (is_array($value)) {
 | 
			
		||||
          $entity_values[$property] = [$langcode => $value];
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    $entity = $this->container->get('entity_type.manager')
 | 
			
		||||
      ->getStorage($this->entityTypeId)
 | 
			
		||||
      ->create($entity_values);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    return $entity->id();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the edit URL for the specified entity.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
 | 
			
		||||
   *   The entity being edited.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Url
 | 
			
		||||
   *   The edit URL.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getEditUrl(ContentEntityInterface $entity) {
 | 
			
		||||
    if ($entity->access('update', $this->loggedInUser)) {
 | 
			
		||||
      $url = $entity->toUrl('edit-form');
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $url = $entity->toUrl('drupal:content-translation-edit');
 | 
			
		||||
      $url->setRouteParameter('language', $entity->language()->getId());
 | 
			
		||||
    }
 | 
			
		||||
    return $url;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,46 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the content translation UI check skip.
 | 
			
		||||
 *
 | 
			
		||||
 * @covers \Drupal\language\Form\ContentLanguageSettingsForm
 | 
			
		||||
 * @covers ::_content_translation_form_language_content_settings_form_alter
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationUISkipTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['content_translation_test', 'user', 'node'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the content_translation_ui_skip key functionality.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUICheckSkip(): void {
 | 
			
		||||
    $admin_user = $this->drupalCreateUser([
 | 
			
		||||
      'translate any entity',
 | 
			
		||||
      'administer content translation',
 | 
			
		||||
      'administer languages',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($admin_user);
 | 
			
		||||
    // Visit the content translation.
 | 
			
		||||
    $this->drupalGet('admin/config/regional/content-language');
 | 
			
		||||
 | 
			
		||||
    // Check the message regarding UI integration.
 | 
			
		||||
    $this->assertSession()->pageTextContains('Test entity - Translatable skip UI check');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Test entity - Translatable check UI (Translation is not supported)');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,633 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Cache\Cache;
 | 
			
		||||
use Drupal\Core\Entity\EntityChangedInterface;
 | 
			
		||||
use Drupal\Core\Entity\EntityInterface;
 | 
			
		||||
use Drupal\Core\Language\Language;
 | 
			
		||||
use Drupal\Core\Language\LanguageInterface;
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
 | 
			
		||||
use Drupal\user\UserInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the Content Translation UI.
 | 
			
		||||
 */
 | 
			
		||||
abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
 | 
			
		||||
 | 
			
		||||
  use AssertPageCacheContextsAndTagsTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The id of the entity being translated.
 | 
			
		||||
   *
 | 
			
		||||
   * @var mixed
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityId;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Whether the behavior of the language selector should be tested.
 | 
			
		||||
   *
 | 
			
		||||
   * @var bool
 | 
			
		||||
   */
 | 
			
		||||
  protected $testLanguageSelector = TRUE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Flag to determine if "all languages" rendering is tested.
 | 
			
		||||
   *
 | 
			
		||||
   * @var bool
 | 
			
		||||
   */
 | 
			
		||||
  protected $testHTMLEscapeForAllLanguages = FALSE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Default cache contexts expected on a non-translated entity.
 | 
			
		||||
   *
 | 
			
		||||
   * Cache contexts will not be checked if this list is empty.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string[]
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'url.query_args:_wrapper_format', 'user.permissions'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the basic translation UI.
 | 
			
		||||
   */
 | 
			
		||||
  public function testTranslationUI(): void {
 | 
			
		||||
    $this->doTestBasicTranslation();
 | 
			
		||||
    $this->doTestTranslationOverview();
 | 
			
		||||
    $this->doTestOutdatedStatus();
 | 
			
		||||
    $this->doTestPublishedStatus();
 | 
			
		||||
    $this->doTestAuthoringInfo();
 | 
			
		||||
    $this->doTestTranslationEdit();
 | 
			
		||||
    $this->doTestTranslationChanged();
 | 
			
		||||
    $this->doTestChangedTimeAfterSaveWithoutChanges();
 | 
			
		||||
    $this->doTestTranslationDeletion();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the basic translation workflow.
 | 
			
		||||
   */
 | 
			
		||||
  protected function doTestBasicTranslation() {
 | 
			
		||||
    // Create a new test entity with original values in the default language.
 | 
			
		||||
    $default_langcode = $this->langcodes[0];
 | 
			
		||||
    $values[$default_langcode] = $this->getNewEntityValues($default_langcode);
 | 
			
		||||
    // Create the entity with the editor as owner, so that afterwards a new
 | 
			
		||||
    // translation is created by the translator and the translation author is
 | 
			
		||||
    // tested.
 | 
			
		||||
    $this->drupalLogin($this->editor);
 | 
			
		||||
    $this->entityId = $this->createEntity($values[$default_langcode], $default_langcode);
 | 
			
		||||
    $this->drupalLogin($this->translator);
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')
 | 
			
		||||
      ->getStorage($this->entityTypeId);
 | 
			
		||||
    $entity = $storage->load($this->entityId);
 | 
			
		||||
    $this->assertNotEmpty($entity, 'Entity found in the database.');
 | 
			
		||||
    $this->drupalGet($entity->toUrl());
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Ensure that the content language cache context is not yet added to the
 | 
			
		||||
    // page.
 | 
			
		||||
    $this->assertCacheContexts($this->defaultCacheContexts);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($entity->toUrl('drupal:content-translation-overview'));
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Source language');
 | 
			
		||||
 | 
			
		||||
    $translation = $this->getTranslation($entity, $default_langcode);
 | 
			
		||||
    foreach ($values[$default_langcode] as $property => $value) {
 | 
			
		||||
      $stored_value = $this->getValue($translation, $property, $default_langcode);
 | 
			
		||||
      $value = is_array($value) ? $value[0]['value'] : $value;
 | 
			
		||||
      $message = "$property correctly stored in the default language.";
 | 
			
		||||
      $this->assertEquals($value, $stored_value, $message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add a content translation.
 | 
			
		||||
    $langcode = 'it';
 | 
			
		||||
    $language = ConfigurableLanguage::load($langcode);
 | 
			
		||||
    $values[$langcode] = $this->getNewEntityValues($langcode);
 | 
			
		||||
 | 
			
		||||
    $entity_type_id = $entity->getEntityTypeId();
 | 
			
		||||
    $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
 | 
			
		||||
      $entity->getEntityTypeId() => $entity->id(),
 | 
			
		||||
      'source' => $default_langcode,
 | 
			
		||||
      'target' => $langcode,
 | 
			
		||||
    ], ['language' => $language]);
 | 
			
		||||
    $this->drupalGet($add_url);
 | 
			
		||||
    $this->submitForm($this->getEditValues($values, $langcode), $this->getFormSubmitActionForNewTranslation($entity, $langcode));
 | 
			
		||||
 | 
			
		||||
    // Assert that HTML is not escaped unexpectedly.
 | 
			
		||||
    if ($this->testHTMLEscapeForAllLanguages) {
 | 
			
		||||
      $this->assertSession()->responseNotContains('<span class="translation-entity-all-languages">(all languages)</span>');
 | 
			
		||||
      $this->assertSession()->responseContains('<span class="translation-entity-all-languages">(all languages)</span>');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Ensure that the content language cache context is not yet added to the
 | 
			
		||||
    // page.
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')
 | 
			
		||||
      ->getStorage($this->entityTypeId);
 | 
			
		||||
    $entity = $storage->load($this->entityId);
 | 
			
		||||
    $this->drupalGet($entity->toUrl());
 | 
			
		||||
    $this->assertCacheContexts(Cache::mergeContexts(['languages:language_content'], $this->defaultCacheContexts));
 | 
			
		||||
 | 
			
		||||
    // Reset the cache of the entity, so that the new translation gets the
 | 
			
		||||
    // updated values.
 | 
			
		||||
    $metadata_source_translation = $this->manager->getTranslationMetadata($entity->getTranslation($default_langcode));
 | 
			
		||||
    $metadata_target_translation = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
 | 
			
		||||
 | 
			
		||||
    $author_field_name = $entity->hasField('content_translation_uid') ? 'content_translation_uid' : 'uid';
 | 
			
		||||
    if ($entity->getFieldDefinition($author_field_name)->isTranslatable()) {
 | 
			
		||||
      $this->assertEquals($this->translator->id(), $metadata_target_translation->getAuthor()->id(), "Author of the target translation $langcode correctly stored for translatable owner field.");
 | 
			
		||||
 | 
			
		||||
      $this->assertNotEquals($metadata_target_translation->getAuthor()->id(), $metadata_source_translation->getAuthor()->id(),
 | 
			
		||||
        "Author of the target translation $langcode different from the author of the source translation $default_langcode for translatable owner field.");
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $this->assertEquals($this->editor->id(), $metadata_target_translation->getAuthor()->id(), 'Author of the entity remained untouched after translation for non translatable owner field.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $created_field_name = $entity->hasField('content_translation_created') ? 'content_translation_created' : 'created';
 | 
			
		||||
    if ($entity->getFieldDefinition($created_field_name)->isTranslatable()) {
 | 
			
		||||
      // Verify that the translation creation timestamp of the target
 | 
			
		||||
      // translation language is newer than the creation timestamp of the source
 | 
			
		||||
      // translation default language for the translatable created field.
 | 
			
		||||
      $this->assertGreaterThan($metadata_source_translation->getCreatedTime(), $metadata_target_translation->getCreatedTime());
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $this->assertEquals($metadata_source_translation->getCreatedTime(), $metadata_target_translation->getCreatedTime(), 'Creation timestamp of the entity remained untouched after translation for non translatable created field.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ($this->testLanguageSelector) {
 | 
			
		||||
      // Verify that language selector is correctly disabled on translations.
 | 
			
		||||
      $this->assertSession()->fieldNotExists('edit-langcode-0-value');
 | 
			
		||||
    }
 | 
			
		||||
    $entity = $storage->load($this->entityId);
 | 
			
		||||
    $this->drupalGet($entity->toUrl('drupal:content-translation-overview'));
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Source language');
 | 
			
		||||
 | 
			
		||||
    // Switch the source language.
 | 
			
		||||
    $langcode = 'fr';
 | 
			
		||||
    $language = ConfigurableLanguage::load($langcode);
 | 
			
		||||
    $source_langcode = 'it';
 | 
			
		||||
    $edit = ['source_langcode[source]' => $source_langcode];
 | 
			
		||||
    $entity_type_id = $entity->getEntityTypeId();
 | 
			
		||||
    $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
 | 
			
		||||
      $entity->getEntityTypeId() => $entity->id(),
 | 
			
		||||
      'source' => $default_langcode,
 | 
			
		||||
      'target' => $langcode,
 | 
			
		||||
    ], ['language' => $language]);
 | 
			
		||||
    // This does not save anything, it merely reloads the form and fills in the
 | 
			
		||||
    // fields with the values from the different source language.
 | 
			
		||||
    $this->drupalGet($add_url);
 | 
			
		||||
    $this->submitForm($edit, 'Change');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals("{$this->fieldName}[0][value]", $values[$source_langcode][$this->fieldName][0]['value']);
 | 
			
		||||
 | 
			
		||||
    // Add another translation and mark the other ones as outdated.
 | 
			
		||||
    $values[$langcode] = $this->getNewEntityValues($langcode);
 | 
			
		||||
    $edit = $this->getEditValues($values, $langcode) + ['content_translation[retranslate]' => TRUE];
 | 
			
		||||
    $entity_type_id = $entity->getEntityTypeId();
 | 
			
		||||
    $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
 | 
			
		||||
      $entity->getEntityTypeId() => $entity->id(),
 | 
			
		||||
      'source' => $source_langcode,
 | 
			
		||||
      'target' => $langcode,
 | 
			
		||||
    ], ['language' => $language]);
 | 
			
		||||
    $this->drupalGet($add_url);
 | 
			
		||||
    $this->submitForm($edit, $this->getFormSubmitActionForNewTranslation($entity, $langcode));
 | 
			
		||||
    $entity = $storage->load($this->entityId);
 | 
			
		||||
    $this->drupalGet($entity->toUrl('drupal:content-translation-overview'));
 | 
			
		||||
    $this->assertSession()->pageTextContains('Source language');
 | 
			
		||||
 | 
			
		||||
    // Check that the entered values have been correctly stored.
 | 
			
		||||
    foreach ($values as $langcode => $property_values) {
 | 
			
		||||
      $translation = $this->getTranslation($entity, $langcode);
 | 
			
		||||
      foreach ($property_values as $property => $value) {
 | 
			
		||||
        $stored_value = $this->getValue($translation, $property, $langcode);
 | 
			
		||||
        $value = is_array($value) ? $value[0]['value'] : $value;
 | 
			
		||||
        $message = "$property correctly stored with language $langcode.";
 | 
			
		||||
        $this->assertEquals($value, $stored_value, $message);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that the translation overview shows the correct values.
 | 
			
		||||
   */
 | 
			
		||||
  protected function doTestTranslationOverview() {
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')
 | 
			
		||||
      ->getStorage($this->entityTypeId);
 | 
			
		||||
    $entity = $storage->load($this->entityId);
 | 
			
		||||
    $translate_url = $entity->toUrl('drupal:content-translation-overview');
 | 
			
		||||
    $this->drupalGet($translate_url);
 | 
			
		||||
    $translate_url->setAbsolute(FALSE);
 | 
			
		||||
 | 
			
		||||
    foreach ($this->langcodes as $langcode) {
 | 
			
		||||
      if ($entity->hasTranslation($langcode)) {
 | 
			
		||||
        $language = new Language(['id' => $langcode]);
 | 
			
		||||
        // Test that label is correctly shown for translation.
 | 
			
		||||
        $view_url = $entity->toUrl('canonical', ['language' => $language])->toString();
 | 
			
		||||
        $this->assertSession()->elementTextEquals('xpath', "//table//a[@href='{$view_url}']", $entity->getTranslation($langcode)->label() ?? $entity->getTranslation($langcode)->id());
 | 
			
		||||
        // Test that edit link is correct for translation.
 | 
			
		||||
        $edit_path = $entity->toUrl('edit-form', ['language' => $language])->toString();
 | 
			
		||||
        $this->assertSession()->elementTextEquals('xpath', "//table//ul[@class='dropbutton']/li/a[@href='{$edit_path}']", 'Edit');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests up-to-date status tracking.
 | 
			
		||||
   */
 | 
			
		||||
  protected function doTestOutdatedStatus() {
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')
 | 
			
		||||
      ->getStorage($this->entityTypeId);
 | 
			
		||||
    $entity = $storage->load($this->entityId);
 | 
			
		||||
    $langcode = 'fr';
 | 
			
		||||
    $languages = \Drupal::languageManager()->getLanguages();
 | 
			
		||||
 | 
			
		||||
    // Mark translations as outdated.
 | 
			
		||||
    $edit = ['content_translation[retranslate]' => TRUE];
 | 
			
		||||
    $edit_path = $entity->toUrl('edit-form', ['language' => $languages[$langcode]]);
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->submitForm($edit, $this->getFormSubmitAction($entity, $langcode));
 | 
			
		||||
    $entity = $storage->load($this->entityId);
 | 
			
		||||
 | 
			
		||||
    // Check that every translation has the correct "outdated" status, and that
 | 
			
		||||
    // the Translation fieldset is open if the translation is "outdated".
 | 
			
		||||
    foreach ($this->langcodes as $added_langcode) {
 | 
			
		||||
      $url = $entity->toUrl('edit-form', ['language' => ConfigurableLanguage::load($added_langcode)]);
 | 
			
		||||
      $this->drupalGet($url);
 | 
			
		||||
      if ($added_langcode == $langcode) {
 | 
			
		||||
        // Verify that the retranslate flag is not checked by default.
 | 
			
		||||
        $this->assertSession()->fieldValueEquals('content_translation[retranslate]', FALSE);
 | 
			
		||||
        $this->assertSession()->elementNotExists('xpath', '//details[@id="edit-content-translation" and @open="open"]');
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        // Verify that the translate flag is checked by default.
 | 
			
		||||
        $this->assertSession()->fieldValueEquals('content_translation[outdated]', TRUE);
 | 
			
		||||
        $this->assertSession()->elementExists('xpath', '//details[@id="edit-content-translation" and @open="open"]');
 | 
			
		||||
        $edit = ['content_translation[outdated]' => FALSE];
 | 
			
		||||
        $this->drupalGet($url);
 | 
			
		||||
        $this->submitForm($edit, $this->getFormSubmitAction($entity, $added_langcode));
 | 
			
		||||
        $this->drupalGet($url);
 | 
			
		||||
        // Verify that retranslate flag is now shown.
 | 
			
		||||
        $this->assertSession()->fieldValueEquals('content_translation[retranslate]', FALSE);
 | 
			
		||||
        $storage = $this->container->get('entity_type.manager')
 | 
			
		||||
          ->getStorage($this->entityTypeId);
 | 
			
		||||
        $storage->resetCache([$this->entityId]);
 | 
			
		||||
        $entity = $storage->load($this->entityId);
 | 
			
		||||
        $this->assertFalse($this->manager->getTranslationMetadata($entity->getTranslation($added_langcode))->isOutdated(), 'The "outdated" status has been correctly stored.');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the translation publishing status.
 | 
			
		||||
   */
 | 
			
		||||
  protected function doTestPublishedStatus() {
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')
 | 
			
		||||
      ->getStorage($this->entityTypeId);
 | 
			
		||||
    $entity = $storage->load($this->entityId);
 | 
			
		||||
 | 
			
		||||
    // Unpublish translations.
 | 
			
		||||
    foreach ($this->langcodes as $index => $langcode) {
 | 
			
		||||
      if ($index > 0) {
 | 
			
		||||
        $url = $entity->toUrl('edit-form', ['language' => ConfigurableLanguage::load($langcode)]);
 | 
			
		||||
        $edit = ['content_translation[status]' => FALSE];
 | 
			
		||||
        $this->drupalGet($url);
 | 
			
		||||
        $this->submitForm($edit, $this->getFormSubmitAction($entity, $langcode));
 | 
			
		||||
        $storage = $this->container->get('entity_type.manager')
 | 
			
		||||
          ->getStorage($this->entityTypeId);
 | 
			
		||||
        $storage->resetCache([$this->entityId]);
 | 
			
		||||
        $entity = $storage->load($this->entityId);
 | 
			
		||||
        $this->assertFalse($this->manager->getTranslationMetadata($entity->getTranslation($langcode))->isPublished(), 'The translation has been correctly unpublished.');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check that the last published translation cannot be unpublished.
 | 
			
		||||
    $this->drupalGet($entity->toUrl('edit-form'));
 | 
			
		||||
    $this->assertSession()->fieldDisabled('content_translation[status]');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('content_translation[status]', TRUE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the translation authoring information.
 | 
			
		||||
   */
 | 
			
		||||
  protected function doTestAuthoringInfo() {
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')
 | 
			
		||||
      ->getStorage($this->entityTypeId);
 | 
			
		||||
    $entity = $storage->load($this->entityId);
 | 
			
		||||
    $values = [];
 | 
			
		||||
 | 
			
		||||
    // Post different authoring information for each translation.
 | 
			
		||||
    foreach ($this->langcodes as $langcode) {
 | 
			
		||||
      $user = $this->drupalCreateUser();
 | 
			
		||||
      $values[$langcode] = [
 | 
			
		||||
        'uid' => $user->id(),
 | 
			
		||||
        'created' => \Drupal::time()->getRequestTime() - mt_rand(0, 1000),
 | 
			
		||||
      ];
 | 
			
		||||
      $edit = [
 | 
			
		||||
        'content_translation[uid]' => $user->getAccountName(),
 | 
			
		||||
        'content_translation[created]' => $this->container->get('date.formatter')->format($values[$langcode]['created'], 'custom', 'Y-m-d H:i:s O'),
 | 
			
		||||
      ];
 | 
			
		||||
      $url = $entity->toUrl('edit-form', ['language' => ConfigurableLanguage::load($langcode)]);
 | 
			
		||||
      $this->drupalGet($url);
 | 
			
		||||
      $this->submitForm($edit, $this->getFormSubmitAction($entity, $langcode));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')
 | 
			
		||||
      ->getStorage($this->entityTypeId);
 | 
			
		||||
    $entity = $storage->load($this->entityId);
 | 
			
		||||
    foreach ($this->langcodes as $langcode) {
 | 
			
		||||
      $metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
 | 
			
		||||
      $this->assertEquals($values[$langcode]['uid'], $metadata->getAuthor()->id(), 'Translation author correctly stored.');
 | 
			
		||||
      $this->assertEquals($values[$langcode]['created'], $metadata->getCreatedTime(), 'Translation date correctly stored.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Try to post non valid values and check that they are rejected.
 | 
			
		||||
    $langcode = 'en';
 | 
			
		||||
    $edit = [
 | 
			
		||||
      // User names have by default length 8.
 | 
			
		||||
      'content_translation[uid]' => $this->randomMachineName(12),
 | 
			
		||||
      'content_translation[created]' => '19/11/1978',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet($entity->toUrl('edit-form'));
 | 
			
		||||
    $this->submitForm($edit, $this->getFormSubmitAction($entity, $langcode));
 | 
			
		||||
    $this->assertSession()->statusMessageExists('error');
 | 
			
		||||
    $metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
 | 
			
		||||
    $this->assertEquals($values[$langcode]['uid'], $metadata->getAuthor()->id(), 'Translation author correctly kept.');
 | 
			
		||||
    $this->assertEquals($values[$langcode]['created'], $metadata->getCreatedTime(), 'Translation date correctly kept.');
 | 
			
		||||
 | 
			
		||||
    // Verify that long usernames can be saved as the translation author.
 | 
			
		||||
    $user = $this->drupalCreateUser([], $this->randomMachineName(UserInterface::USERNAME_MAX_LENGTH));
 | 
			
		||||
    $edit = [
 | 
			
		||||
      // Format the username as it is entered in autocomplete fields.
 | 
			
		||||
      'content_translation[uid]' => $user->getAccountName() . ' (' . $user->id() . ')',
 | 
			
		||||
      'content_translation[created]' => $this->container->get('date.formatter')->format($values[$langcode]['created'], 'custom', 'Y-m-d H:i:s O'),
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, $this->getFormSubmitAction($entity, $langcode));
 | 
			
		||||
    $reloaded_entity = $storage->load($this->entityId);
 | 
			
		||||
    $metadata = $this->manager->getTranslationMetadata($reloaded_entity->getTranslation($langcode));
 | 
			
		||||
    $this->assertEquals($user->id(), $metadata->getAuthor()->id(), 'Translation author correctly set.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests translation deletion.
 | 
			
		||||
   */
 | 
			
		||||
  protected function doTestTranslationDeletion() {
 | 
			
		||||
    // Confirm and delete a translation.
 | 
			
		||||
    $this->drupalLogin($this->translator);
 | 
			
		||||
    $langcode = 'fr';
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')
 | 
			
		||||
      ->getStorage($this->entityTypeId);
 | 
			
		||||
    $entity = $storage->load($this->entityId);
 | 
			
		||||
    $language = ConfigurableLanguage::load($langcode);
 | 
			
		||||
    $url = $entity->toUrl('edit-form', ['language' => $language]);
 | 
			
		||||
    $this->drupalGet($url);
 | 
			
		||||
    $this->clickLink('Delete translation');
 | 
			
		||||
    $this->submitForm([], 'Delete ' . $language->getName() . ' translation');
 | 
			
		||||
    $entity = $storage->load($this->entityId, TRUE);
 | 
			
		||||
    $this->assertIsObject($entity);
 | 
			
		||||
    $translations = $entity->getTranslationLanguages();
 | 
			
		||||
    $this->assertCount(2, $translations);
 | 
			
		||||
    $this->assertArrayNotHasKey($langcode, $translations);
 | 
			
		||||
 | 
			
		||||
    // Check that the translator cannot delete the original translation.
 | 
			
		||||
    $args = [$this->entityTypeId => $entity->id(), 'language' => 'en'];
 | 
			
		||||
    $this->drupalGet(Url::fromRoute("entity.$this->entityTypeId.content_translation_delete", $args));
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns an array of entity field values to be tested.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getNewEntityValues($langcode) {
 | 
			
		||||
    return [$this->fieldName => [['value' => $this->randomMachineName(16)]]];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns an edit array containing the values to be posted.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getEditValues($values, $langcode, $new = FALSE) {
 | 
			
		||||
    $edit = $values[$langcode];
 | 
			
		||||
    $langcode = $new ? LanguageInterface::LANGCODE_NOT_SPECIFIED : $langcode;
 | 
			
		||||
    foreach ($values[$langcode] as $property => $value) {
 | 
			
		||||
      if (is_array($value)) {
 | 
			
		||||
        $edit["{$property}[0][value]"] = $value[0]['value'];
 | 
			
		||||
        unset($edit[$property]);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return $edit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the form action value when submitting a new translation.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Entity\EntityInterface $entity
 | 
			
		||||
   *   The entity being tested.
 | 
			
		||||
   * @param string $langcode
 | 
			
		||||
   *   Language code for the form.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   Name of the button to hit.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getFormSubmitActionForNewTranslation(EntityInterface $entity, $langcode) {
 | 
			
		||||
    $entity->addTranslation($langcode, $entity->toArray());
 | 
			
		||||
    return $this->getFormSubmitAction($entity, $langcode);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the form action value to be used to submit the entity form.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Entity\EntityInterface $entity
 | 
			
		||||
   *   The entity being tested.
 | 
			
		||||
   * @param string $langcode
 | 
			
		||||
   *   Language code for the form.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   Name of the button to hit.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getFormSubmitAction(EntityInterface $entity, $langcode) {
 | 
			
		||||
    return 'Save' . $this->getFormSubmitSuffix($entity, $langcode);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns appropriate submit button suffix based on translatability.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Entity\EntityInterface $entity
 | 
			
		||||
   *   The entity being tested.
 | 
			
		||||
   * @param string $langcode
 | 
			
		||||
   *   Language code for the form.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   Submit button suffix based on translatability.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getFormSubmitSuffix(EntityInterface $entity, $langcode) {
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the translation object to use to retrieve the translated values.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Entity\EntityInterface $entity
 | 
			
		||||
   *   The entity being tested.
 | 
			
		||||
   * @param string $langcode
 | 
			
		||||
   *   The language code identifying the translation to be retrieved.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\TypedData\TranslatableInterface
 | 
			
		||||
   *   The translation object to act on.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getTranslation(EntityInterface $entity, $langcode) {
 | 
			
		||||
    return $entity->getTranslation($langcode);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the value for the specified property in the given language.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Entity\EntityInterface $translation
 | 
			
		||||
   *   The translation object the property value should be retrieved from.
 | 
			
		||||
   * @param string $property
 | 
			
		||||
   *   The property name.
 | 
			
		||||
   * @param string $langcode
 | 
			
		||||
   *   The property value.
 | 
			
		||||
   *
 | 
			
		||||
   * @return mixed
 | 
			
		||||
   *   The property value.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getValue(EntityInterface $translation, $property, $langcode) {
 | 
			
		||||
    $key = $property == 'user_id' ? 'target_id' : 'value';
 | 
			
		||||
    return $translation->get($property)->{$key};
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the name of the field that implements the changed timestamp.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Entity\EntityInterface $entity
 | 
			
		||||
   *   The entity being tested.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   The field name.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getChangedFieldName($entity) {
 | 
			
		||||
    return $entity->hasField('content_translation_changed') ? 'content_translation_changed' : 'changed';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests edit content translation.
 | 
			
		||||
   */
 | 
			
		||||
  protected function doTestTranslationEdit() {
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')
 | 
			
		||||
      ->getStorage($this->entityTypeId);
 | 
			
		||||
    $entity = $storage->load($this->entityId);
 | 
			
		||||
    $languages = $this->container->get('language_manager')->getLanguages();
 | 
			
		||||
 | 
			
		||||
    foreach ($this->langcodes as $langcode) {
 | 
			
		||||
      // We only want to test the title for non-english translations.
 | 
			
		||||
      if ($langcode != 'en') {
 | 
			
		||||
        $options = ['language' => $languages[$langcode]];
 | 
			
		||||
        $url = $entity->toUrl('edit-form', $options);
 | 
			
		||||
        $this->drupalGet($url);
 | 
			
		||||
 | 
			
		||||
        $this->assertSession()->responseContains($entity->getTranslation($langcode)->label());
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the basic translation workflow.
 | 
			
		||||
   */
 | 
			
		||||
  protected function doTestTranslationChanged() {
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')
 | 
			
		||||
      ->getStorage($this->entityTypeId);
 | 
			
		||||
    $entity = $storage->load($this->entityId);
 | 
			
		||||
    $changed_field_name = $this->getChangedFieldName($entity);
 | 
			
		||||
    $definition = $entity->getFieldDefinition($changed_field_name);
 | 
			
		||||
    $config = $definition->getConfig($entity->bundle());
 | 
			
		||||
 | 
			
		||||
    foreach ([FALSE, TRUE] as $translatable_changed_field) {
 | 
			
		||||
      if ($definition->isTranslatable()) {
 | 
			
		||||
        // For entities defining a translatable changed field we want to test
 | 
			
		||||
        // the correct behavior of that field even if the translatability is
 | 
			
		||||
        // revoked. In that case the changed timestamp should be synchronized
 | 
			
		||||
        // across all translations.
 | 
			
		||||
        $config->setTranslatable($translatable_changed_field);
 | 
			
		||||
        $config->save();
 | 
			
		||||
      }
 | 
			
		||||
      elseif ($translatable_changed_field) {
 | 
			
		||||
        // For entities defining a non-translatable changed field we cannot
 | 
			
		||||
        // declare the field as translatable on the fly by modifying its config
 | 
			
		||||
        // because the schema doesn't support this.
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      foreach ($entity->getTranslationLanguages() as $language) {
 | 
			
		||||
        // Ensure different timestamps.
 | 
			
		||||
        sleep(1);
 | 
			
		||||
 | 
			
		||||
        $langcode = $language->getId();
 | 
			
		||||
 | 
			
		||||
        $edit = [
 | 
			
		||||
          $this->fieldName . '[0][value]' => $this->randomString(),
 | 
			
		||||
        ];
 | 
			
		||||
        $edit_path = $entity->toUrl('edit-form', ['language' => $language]);
 | 
			
		||||
        $this->drupalGet($edit_path);
 | 
			
		||||
        $this->submitForm($edit, $this->getFormSubmitAction($entity, $langcode));
 | 
			
		||||
 | 
			
		||||
        $storage = $this->container->get('entity_type.manager')
 | 
			
		||||
          ->getStorage($this->entityTypeId);
 | 
			
		||||
        $storage->resetCache([$this->entityId]);
 | 
			
		||||
        $entity = $storage->load($this->entityId);
 | 
			
		||||
        $this->assertEquals(
 | 
			
		||||
          $entity->getChangedTimeAcrossTranslations(), $entity->getTranslation($langcode)->getChangedTime(),
 | 
			
		||||
          "Changed time for language {$language->getName()} is the latest change over all languages."
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      $timestamps = [];
 | 
			
		||||
      foreach ($entity->getTranslationLanguages() as $language) {
 | 
			
		||||
        $next_timestamp = $entity->getTranslation($language->getId())->getChangedTime();
 | 
			
		||||
        if (!in_array($next_timestamp, $timestamps)) {
 | 
			
		||||
          $timestamps[] = $next_timestamp;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if ($translatable_changed_field) {
 | 
			
		||||
        $this->assertSameSize($entity->getTranslationLanguages(), $timestamps, 'All timestamps from all languages are different.');
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        $this->assertCount(1, $timestamps, 'All timestamps from all languages are identical.');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the changed time after API and FORM save without changes.
 | 
			
		||||
   */
 | 
			
		||||
  public function doTestChangedTimeAfterSaveWithoutChanges() {
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')
 | 
			
		||||
      ->getStorage($this->entityTypeId);
 | 
			
		||||
    $entity = $storage->load($this->entityId);
 | 
			
		||||
    // Test only entities, which implement the EntityChangedInterface.
 | 
			
		||||
    if ($entity instanceof EntityChangedInterface) {
 | 
			
		||||
      $changed_timestamp = $entity->getChangedTime();
 | 
			
		||||
 | 
			
		||||
      $entity->save();
 | 
			
		||||
      $storage = $this->container->get('entity_type.manager')
 | 
			
		||||
        ->getStorage($this->entityTypeId);
 | 
			
		||||
      $storage->resetCache([$this->entityId]);
 | 
			
		||||
      $entity = $storage->load($this->entityId);
 | 
			
		||||
      $this->assertEquals($changed_timestamp, $entity->getChangedTime(), 'The entity\'s changed time wasn\'t updated after API save without changes.');
 | 
			
		||||
 | 
			
		||||
      // Ensure different save timestamps.
 | 
			
		||||
      sleep(1);
 | 
			
		||||
 | 
			
		||||
      // Save the entity on the regular edit form.
 | 
			
		||||
      $language = $entity->language();
 | 
			
		||||
      $edit_path = $entity->toUrl('edit-form', ['language' => $language]);
 | 
			
		||||
      $this->drupalGet($edit_path);
 | 
			
		||||
      $this->submitForm([], $this->getFormSubmitAction($entity, $language->getId()));
 | 
			
		||||
 | 
			
		||||
      $storage->resetCache([$this->entityId]);
 | 
			
		||||
      $entity = $storage->load($this->entityId);
 | 
			
		||||
      $this->assertNotEquals($changed_timestamp, $entity->getChangedTime(), 'The entity\'s changed time was updated after form save without changes.');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,190 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\Tests\language\Traits\LanguageTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the untranslatable fields behaviors.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationUntranslatableFieldsTest extends ContentTranslationPendingRevisionTestBase {
 | 
			
		||||
 | 
			
		||||
  use LanguageTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['field_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->doSetup();
 | 
			
		||||
 | 
			
		||||
    // Configure one field as untranslatable.
 | 
			
		||||
    $this->drupalLogin($this->administrator);
 | 
			
		||||
    static::setFieldTranslatable($this->entityTypeId, $this->bundle, $this->fieldName, FALSE);
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */
 | 
			
		||||
    $entity_field_manager = $this->container->get('entity_field.manager');
 | 
			
		||||
    $entity_field_manager->clearCachedFieldDefinitions();
 | 
			
		||||
    $definitions = $entity_field_manager->getFieldDefinitions($this->entityTypeId, $this->bundle);
 | 
			
		||||
    $this->assertFalse($definitions[$this->fieldName]->isTranslatable());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setupTestFields(): void {
 | 
			
		||||
    parent::setupTestFields();
 | 
			
		||||
 | 
			
		||||
    $field_storage = FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => 'field_multilingual',
 | 
			
		||||
      'type' => 'test_field',
 | 
			
		||||
      'entity_type' => $this->entityTypeId,
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
    ]);
 | 
			
		||||
    $field_storage->save();
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'field_storage' => $field_storage,
 | 
			
		||||
      'bundle' => $this->bundle,
 | 
			
		||||
      'label' => 'Untranslatable-but-visible test field',
 | 
			
		||||
      'translatable' => FALSE,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    \Drupal::service('entity_display.repository')->getFormDisplay($this->entityTypeId, $this->bundle, 'default')
 | 
			
		||||
      ->setComponent('field_multilingual', [
 | 
			
		||||
        'type' => 'test_field_widget_multilingual',
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that hiding untranslatable field widgets works correctly.
 | 
			
		||||
   */
 | 
			
		||||
  public function testHiddenWidgets(): void {
 | 
			
		||||
    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
 | 
			
		||||
    $entity_type_manager = $this->container->get('entity_type.manager');
 | 
			
		||||
    $id = $this->createEntity(['title' => $this->randomString()], 'en');
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
 | 
			
		||||
    $entity = $entity_type_manager
 | 
			
		||||
      ->getStorage($this->entityTypeId)
 | 
			
		||||
      ->load($id);
 | 
			
		||||
 | 
			
		||||
    // Check that the untranslatable field widget is displayed on the edit form
 | 
			
		||||
    // and no translatability clue is displayed yet.
 | 
			
		||||
    $en_edit_url = $entity->toUrl('edit-form');
 | 
			
		||||
    $this->drupalGet($en_edit_url);
 | 
			
		||||
    $field_xpath = '//input[@name="' . $this->fieldName . '[0][value]"]';
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', $field_xpath);
 | 
			
		||||
    $clue_xpath = '//label[@for="edit-' . strtr($this->fieldName, '_', '-') . '-0-value"]/span[text()="(all languages)"]';
 | 
			
		||||
    $this->assertSession()->elementNotExists('xpath', $clue_xpath);
 | 
			
		||||
    $this->assertSession()->pageTextContains('Untranslatable-but-visible test field');
 | 
			
		||||
 | 
			
		||||
    // Add a translation and check that the untranslatable field widget is
 | 
			
		||||
    // displayed on the translation and edit forms along with translatability
 | 
			
		||||
    // clues.
 | 
			
		||||
    $add_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add", [
 | 
			
		||||
      $entity->getEntityTypeId() => $entity->id(),
 | 
			
		||||
      'source' => 'en',
 | 
			
		||||
      'target' => 'it',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalGet($add_url);
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', $field_xpath);
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', $clue_xpath);
 | 
			
		||||
    $this->assertSession()->pageTextContains('Untranslatable-but-visible test field');
 | 
			
		||||
    $this->submitForm([], 'Save');
 | 
			
		||||
 | 
			
		||||
    // Check that the widget is displayed along with its clue in the edit form
 | 
			
		||||
    // for both languages.
 | 
			
		||||
    $this->drupalGet($en_edit_url);
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', $field_xpath);
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', $clue_xpath);
 | 
			
		||||
    $it_edit_url = $entity->toUrl('edit-form', ['language' => ConfigurableLanguage::load('it')]);
 | 
			
		||||
    $this->drupalGet($it_edit_url);
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', $field_xpath);
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', $clue_xpath);
 | 
			
		||||
 | 
			
		||||
    // Configure untranslatable field widgets to be hidden on non-default
 | 
			
		||||
    // language edit forms.
 | 
			
		||||
    $settings_key = 'settings[' . $this->entityTypeId . '][' . $this->bundle . '][settings][content_translation][untranslatable_fields_hide]';
 | 
			
		||||
    $settings_url = 'admin/config/regional/content-language';
 | 
			
		||||
    $this->drupalGet($settings_url);
 | 
			
		||||
    $this->submitForm([$settings_key => 1], 'Save configuration');
 | 
			
		||||
 | 
			
		||||
    // Verify that the widget is displayed in the default language edit form,
 | 
			
		||||
    // but no clue is displayed.
 | 
			
		||||
    $this->drupalGet($en_edit_url);
 | 
			
		||||
    $field_xpath = '//input[@name="' . $this->fieldName . '[0][value]"]';
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', $field_xpath);
 | 
			
		||||
    $this->assertSession()->elementNotExists('xpath', $clue_xpath);
 | 
			
		||||
    $this->assertSession()->pageTextContains('Untranslatable-but-visible test field');
 | 
			
		||||
 | 
			
		||||
    // Verify no widget is displayed on the non-default language edit form.
 | 
			
		||||
    $this->drupalGet($it_edit_url);
 | 
			
		||||
    $this->assertSession()->elementNotExists('xpath', $field_xpath);
 | 
			
		||||
    $this->assertSession()->elementNotExists('xpath', $clue_xpath);
 | 
			
		||||
    $this->assertSession()->pageTextContains('Untranslatable-but-visible test field');
 | 
			
		||||
 | 
			
		||||
    // Verify a warning is displayed.
 | 
			
		||||
    $this->assertSession()->statusMessageContains('Fields that apply to all languages are hidden to avoid conflicting changes.', 'warning');
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', '//a[@href="' . $entity->toUrl('edit-form')->toString() . '" and text()="Edit them on the original language form"]');
 | 
			
		||||
 | 
			
		||||
    // Configure untranslatable field widgets to be displayed on non-default
 | 
			
		||||
    // language edit forms.
 | 
			
		||||
    $this->drupalGet($settings_url);
 | 
			
		||||
    $this->submitForm([$settings_key => 0], 'Save configuration');
 | 
			
		||||
 | 
			
		||||
    // Check that the widget is displayed along with its clue in the edit form
 | 
			
		||||
    // for both languages.
 | 
			
		||||
    $this->drupalGet($en_edit_url);
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', $field_xpath);
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', $clue_xpath);
 | 
			
		||||
    $this->drupalGet($it_edit_url);
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', $field_xpath);
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', $clue_xpath);
 | 
			
		||||
 | 
			
		||||
    // Enable content moderation and verify that widgets are hidden despite them
 | 
			
		||||
    // being configured to be displayed.
 | 
			
		||||
    $this->enableContentModeration();
 | 
			
		||||
    $this->drupalGet($it_edit_url);
 | 
			
		||||
    $this->assertSession()->elementNotExists('xpath', $field_xpath);
 | 
			
		||||
    $this->assertSession()->elementNotExists('xpath', $clue_xpath);
 | 
			
		||||
 | 
			
		||||
    // Verify a warning is displayed.
 | 
			
		||||
    $this->assertSession()->statusMessageContains('Fields that apply to all languages are hidden to avoid conflicting changes.', 'warning');
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', '//a[@href="' . $entity->toUrl('edit-form')->toString() . '" and text()="Edit them on the original language form"]');
 | 
			
		||||
 | 
			
		||||
    // Verify that checkboxes on the language content settings page are checked
 | 
			
		||||
    // and disabled for moderated bundles.
 | 
			
		||||
    $this->drupalGet($settings_url);
 | 
			
		||||
    $field_name = "settings[{$this->entityTypeId}][{$this->bundle}][settings][content_translation][untranslatable_fields_hide]";
 | 
			
		||||
    $this->assertSession()->fieldValueEquals($field_name, 1);
 | 
			
		||||
    $this->assertSession()->fieldDisabled($field_name);
 | 
			
		||||
    $this->submitForm([$settings_key => 0], 'Save configuration');
 | 
			
		||||
    $this->assertSession()->fieldValueEquals($field_name, 1);
 | 
			
		||||
    $this->assertSession()->fieldDisabled($field_name);
 | 
			
		||||
 | 
			
		||||
    // Verify that the untranslatable fields warning message is not displayed
 | 
			
		||||
    // when submitting.
 | 
			
		||||
    $this->drupalGet($it_edit_url);
 | 
			
		||||
    $this->assertSession()->pageTextContains('Fields that apply to all languages are hidden to avoid conflicting changes.');
 | 
			
		||||
    $this->submitForm([], 'Save (this translation)');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Fields that apply to all languages are hidden to avoid conflicting changes.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,428 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Language\LanguageInterface;
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestMulRevPub;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
 | 
			
		||||
use Drupal\user\UserInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the content translation workflows for the test entity.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationWorkflowsTest extends ContentTranslationTestBase {
 | 
			
		||||
 | 
			
		||||
  use AssertPageCacheContextsAndTagsTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The entity used for testing.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\entity_test\Entity\EntityTestMul
 | 
			
		||||
   */
 | 
			
		||||
  protected $entity;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityTypeId = 'entity_test_mulrevpub';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The referencing entity.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\entity_test\Entity\EntityTestMul
 | 
			
		||||
   */
 | 
			
		||||
  protected $referencingEntity;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The entity owner account to be used to test multilingual entity editing.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\user\UserInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityOwner;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The user that has entity owner permission but is not the owner.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\user\UserInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $notEntityOwner;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->doSetup();
 | 
			
		||||
 | 
			
		||||
    $field_storage = FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => 'field_reference',
 | 
			
		||||
      'type' => 'entity_reference',
 | 
			
		||||
      'entity_type' => $this->entityTypeId,
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
      'settings' => [
 | 
			
		||||
        'target_type' => $this->entityTypeId,
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
    $field_storage->save();
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'field_storage' => $field_storage,
 | 
			
		||||
      'bundle' => $this->entityTypeId,
 | 
			
		||||
      'label' => 'Reference',
 | 
			
		||||
      'translatable' => FALSE,
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    $this->container->get('entity_display.repository')
 | 
			
		||||
      ->getViewDisplay($this->entityTypeId, $this->entityTypeId, 'default')
 | 
			
		||||
      ->setComponent('field_reference', [
 | 
			
		||||
        'type' => 'entity_reference_entity_view',
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    $this->setupEntity();
 | 
			
		||||
 | 
			
		||||
    // Create a second entity that references the first to test how the
 | 
			
		||||
    // translation can be viewed through an entity reference field.
 | 
			
		||||
    $this->referencingEntity = EntityTestMulRevPub::create([
 | 
			
		||||
      'name' => 'referencing',
 | 
			
		||||
      'field_reference' => $this->entity->id(),
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->referencingEntity->addTranslation($this->langcodes[2], $this->referencingEntity->toArray());
 | 
			
		||||
    $this->referencingEntity->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setupUsers(): void {
 | 
			
		||||
    $this->entityOwner = $this->drupalCreateUser($this->getEntityOwnerPermissions(), 'entity_owner');
 | 
			
		||||
    $this->notEntityOwner = $this->drupalCreateUser();
 | 
			
		||||
    $this->notEntityOwner->set('roles', $this->entityOwner->getRoles(TRUE));
 | 
			
		||||
    $this->notEntityOwner->save();
 | 
			
		||||
    parent::setupUsers();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns an array of permissions needed for the entity owner.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getEntityOwnerPermissions(): array {
 | 
			
		||||
    return ['edit own entity_test content', 'translate editable entities', 'view test entity', 'view test entity translations', 'view unpublished test entity translations'];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getTranslatorPermissions() {
 | 
			
		||||
    $permissions = parent::getTranslatorPermissions();
 | 
			
		||||
    $permissions[] = 'view test entity';
 | 
			
		||||
    $permissions[] = 'view test entity translations';
 | 
			
		||||
    $permissions[] = 'view unpublished test entity translations';
 | 
			
		||||
 | 
			
		||||
    return $permissions;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getEditorPermissions(): array {
 | 
			
		||||
    return ['administer entity_test content', 'view test entity', 'view test entity translations'];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a test entity and translate it.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\User\UserInterface|null $user
 | 
			
		||||
   *   (optional) The entity owner.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setupEntity(?UserInterface $user = NULL): void {
 | 
			
		||||
    $default_langcode = $this->langcodes[0];
 | 
			
		||||
 | 
			
		||||
    // Create a test entity.
 | 
			
		||||
    $user = $user ?: $this->drupalCreateUser();
 | 
			
		||||
    $values = [
 | 
			
		||||
      'name' => $this->randomMachineName(),
 | 
			
		||||
      'user_id' => $user->id(),
 | 
			
		||||
      $this->fieldName => [['value' => $this->randomMachineName(16)]],
 | 
			
		||||
    ];
 | 
			
		||||
    $id = $this->createEntity($values, $default_langcode);
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')
 | 
			
		||||
      ->getStorage($this->entityTypeId);
 | 
			
		||||
 | 
			
		||||
    // Create a translation that is not published to test view access.
 | 
			
		||||
    $this->drupalLogin($this->translator);
 | 
			
		||||
    $add_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_add", [$this->entityTypeId => $id, 'source' => $default_langcode, 'target' => $this->langcodes[2]]);
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'name[0][value]' => 'translation name',
 | 
			
		||||
      'content_translation[status]' => FALSE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet($add_translation_url);
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
 | 
			
		||||
    $storage->resetCache([$id]);
 | 
			
		||||
    $this->entity = $storage->load($id);
 | 
			
		||||
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests simple and editorial translation workflows.
 | 
			
		||||
   */
 | 
			
		||||
  public function testWorkflows(): void {
 | 
			
		||||
    // Test workflows for the editor.
 | 
			
		||||
    $expected_status = [
 | 
			
		||||
      'edit' => 200,
 | 
			
		||||
      'delete' => 200,
 | 
			
		||||
      'overview' => 403,
 | 
			
		||||
      'add_translation' => 403,
 | 
			
		||||
      'edit_translation' => 403,
 | 
			
		||||
      'delete_translation' => 403,
 | 
			
		||||
      'view_unpublished_translation' => 403,
 | 
			
		||||
      'view_unpublished_translation_reference' => FALSE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->doTestWorkflows($this->editor, $expected_status);
 | 
			
		||||
 | 
			
		||||
    // Test workflows for the translator.
 | 
			
		||||
    $expected_status = [
 | 
			
		||||
      'edit' => 403,
 | 
			
		||||
      'delete' => 403,
 | 
			
		||||
      'overview' => 200,
 | 
			
		||||
      'add_translation' => 200,
 | 
			
		||||
      'edit_translation' => 200,
 | 
			
		||||
      'delete_translation' => 200,
 | 
			
		||||
      'view_unpublished_translation' => 200,
 | 
			
		||||
      'view_unpublished_translation_reference' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->doTestWorkflows($this->translator, $expected_status);
 | 
			
		||||
 | 
			
		||||
    // Test workflows for the admin.
 | 
			
		||||
    $expected_status = [
 | 
			
		||||
      'edit' => 200,
 | 
			
		||||
      'delete' => 200,
 | 
			
		||||
      'overview' => 200,
 | 
			
		||||
      'add_translation' => 200,
 | 
			
		||||
      'edit_translation' => 403,
 | 
			
		||||
      'delete_translation' => 403,
 | 
			
		||||
      'view_unpublished_translation' => 200,
 | 
			
		||||
      'view_unpublished_translation_reference' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->doTestWorkflows($this->administrator, $expected_status);
 | 
			
		||||
 | 
			
		||||
    // Check that translation permissions allow the associated operations.
 | 
			
		||||
    $ops = ['create' => 'Add', 'update' => 'Edit', 'delete' => 'Delete'];
 | 
			
		||||
    $translations_url = $this->entity->toUrl('drupal:content-translation-overview');
 | 
			
		||||
    foreach ($ops as $current_op => $item) {
 | 
			
		||||
      $user = $this->drupalCreateUser([
 | 
			
		||||
        $this->getTranslatePermission(),
 | 
			
		||||
        "$current_op content translations",
 | 
			
		||||
        'view test entity',
 | 
			
		||||
      ]);
 | 
			
		||||
      $this->drupalLogin($user);
 | 
			
		||||
      $this->drupalGet($translations_url);
 | 
			
		||||
 | 
			
		||||
      // Make sure that the user.permissions cache context and the cache tags
 | 
			
		||||
      // for the entity are present.
 | 
			
		||||
      $this->assertCacheContext('user.permissions');
 | 
			
		||||
      foreach ($this->entity->getCacheTags() as $cache_tag) {
 | 
			
		||||
        $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', $cache_tag);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      foreach ($ops as $op => $label) {
 | 
			
		||||
        if ($op != $current_op) {
 | 
			
		||||
          $this->assertSession()->linkNotExists($label);
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          $this->assertSession()->linkExists($label, 0);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Test workflows for the entity owner with non-editable content.
 | 
			
		||||
    $expected_status = [
 | 
			
		||||
      'edit' => 403,
 | 
			
		||||
      'delete' => 403,
 | 
			
		||||
      'overview' => 403,
 | 
			
		||||
      'add_translation' => 403,
 | 
			
		||||
      'edit_translation' => 403,
 | 
			
		||||
      'delete_translation' => 403,
 | 
			
		||||
      'view_unpublished_translation' => 200,
 | 
			
		||||
      'view_unpublished_translation_reference' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->doTestWorkflows($this->entityOwner, $expected_status);
 | 
			
		||||
 | 
			
		||||
    // Test workflows for the entity owner with editable content.
 | 
			
		||||
    $this->setupEntity($this->entityOwner);
 | 
			
		||||
    $this->referencingEntity->set('field_reference', $this->entity->id());
 | 
			
		||||
    $this->referencingEntity->save();
 | 
			
		||||
    $expected_status = [
 | 
			
		||||
      'edit' => 200,
 | 
			
		||||
      'delete' => 403,
 | 
			
		||||
      'overview' => 200,
 | 
			
		||||
      'add_translation' => 200,
 | 
			
		||||
      'edit_translation' => 200,
 | 
			
		||||
      'delete_translation' => 200,
 | 
			
		||||
      'view_unpublished_translation' => 200,
 | 
			
		||||
      'view_unpublished_translation_reference' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->doTestWorkflows($this->entityOwner, $expected_status);
 | 
			
		||||
    $expected_status = [
 | 
			
		||||
      'edit' => 403,
 | 
			
		||||
      'delete' => 403,
 | 
			
		||||
      'overview' => 403,
 | 
			
		||||
      'add_translation' => 403,
 | 
			
		||||
      'edit_translation' => 403,
 | 
			
		||||
      'delete_translation' => 403,
 | 
			
		||||
      'view_unpublished_translation' => 200,
 | 
			
		||||
      'view_unpublished_translation_reference' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->doTestWorkflows($this->notEntityOwner, $expected_status);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Checks that workflows have the expected behaviors for the given user.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\user\UserInterface $user
 | 
			
		||||
   *   The user to test the workflow behavior against.
 | 
			
		||||
   * @param array $expected_status
 | 
			
		||||
   *   The an associative array with the operation name as key and the expected
 | 
			
		||||
   *   status as value.
 | 
			
		||||
   */
 | 
			
		||||
  protected function doTestWorkflows(UserInterface $user, $expected_status): void {
 | 
			
		||||
    $default_langcode = $this->langcodes[0];
 | 
			
		||||
    $languages = $this->container->get('language_manager')->getLanguages();
 | 
			
		||||
    $options = ['language' => $languages[$default_langcode], 'absolute' => TRUE];
 | 
			
		||||
    $this->drupalLogin($user);
 | 
			
		||||
 | 
			
		||||
    // Check whether the user is allowed to access the entity form in edit mode.
 | 
			
		||||
    $edit_url = $this->entity->toUrl('edit-form', $options);
 | 
			
		||||
    $this->drupalGet($edit_url, $options);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals($expected_status['edit']);
 | 
			
		||||
 | 
			
		||||
    // Check whether the user is allowed to access the entity delete form.
 | 
			
		||||
    $delete_url = $this->entity->toUrl('delete-form', $options);
 | 
			
		||||
    $this->drupalGet($delete_url, $options);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals($expected_status['delete']);
 | 
			
		||||
 | 
			
		||||
    // Check whether the user is allowed to access the translation overview.
 | 
			
		||||
    $langcode = $this->langcodes[1];
 | 
			
		||||
    $options['language'] = $languages[$langcode];
 | 
			
		||||
    $translations_url = $this->entity->toUrl('drupal:content-translation-overview', $options)->toString();
 | 
			
		||||
    $this->drupalGet($translations_url);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals($expected_status['overview']);
 | 
			
		||||
 | 
			
		||||
    // Check whether the user is allowed to create a translation.
 | 
			
		||||
    $add_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_add", [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $langcode], $options);
 | 
			
		||||
    if ($expected_status['add_translation'] == 200) {
 | 
			
		||||
      $this->clickLink('Add');
 | 
			
		||||
      $this->assertSession()->addressEquals($add_translation_url);
 | 
			
		||||
      // Check that the translation form does not contain shared elements for
 | 
			
		||||
      // translators.
 | 
			
		||||
      if ($expected_status['edit'] == 403) {
 | 
			
		||||
        $this->assertNoSharedElements();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $this->drupalGet($add_translation_url);
 | 
			
		||||
    }
 | 
			
		||||
    $this->assertSession()->statusCodeEquals($expected_status['add_translation']);
 | 
			
		||||
 | 
			
		||||
    // Check whether the user is allowed to edit a translation.
 | 
			
		||||
    $langcode = $this->langcodes[2];
 | 
			
		||||
    $options['language'] = $languages[$langcode];
 | 
			
		||||
    $edit_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_edit", [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options);
 | 
			
		||||
    if ($expected_status['edit_translation'] == 200) {
 | 
			
		||||
      $this->drupalGet($translations_url);
 | 
			
		||||
      $editor = $expected_status['edit'] == 200;
 | 
			
		||||
 | 
			
		||||
      if ($editor) {
 | 
			
		||||
        $this->clickLink('Edit', 1);
 | 
			
		||||
        // An editor should be pointed to the entity form in multilingual mode.
 | 
			
		||||
        // We need a new expected edit path with a new language.
 | 
			
		||||
        $expected_edit_path = $this->entity->toUrl('edit-form', $options)->toString();
 | 
			
		||||
        $this->assertSession()->addressEquals($expected_edit_path);
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        $this->clickLink('Edit');
 | 
			
		||||
        // While a translator should be pointed to the translation form.
 | 
			
		||||
        $this->assertSession()->addressEquals($edit_translation_url);
 | 
			
		||||
        // Check that the translation form does not contain shared elements.
 | 
			
		||||
        $this->assertNoSharedElements();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $this->drupalGet($edit_translation_url);
 | 
			
		||||
    }
 | 
			
		||||
    $this->assertSession()->statusCodeEquals($expected_status['edit_translation']);
 | 
			
		||||
 | 
			
		||||
    // When viewing an unpublished entity directly, access is currently denied
 | 
			
		||||
    // completely. See https://www.drupal.org/node/2978048.
 | 
			
		||||
    $this->drupalGet($this->entity->getTranslation($langcode)->toUrl());
 | 
			
		||||
    $this->assertSession()->statusCodeEquals($expected_status['view_unpublished_translation']);
 | 
			
		||||
 | 
			
		||||
    // On a reference field, the translation falls back to the default language.
 | 
			
		||||
    $this->drupalGet($this->referencingEntity->getTranslation($langcode)->toUrl());
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    if ($expected_status['view_unpublished_translation_reference']) {
 | 
			
		||||
      $this->assertSession()->pageTextContains('translation name');
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $this->assertSession()->pageTextContains($this->entity->label());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check whether the user is allowed to delete a translation.
 | 
			
		||||
    $delete_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_delete", [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options);
 | 
			
		||||
    if ($expected_status['delete_translation'] == 200) {
 | 
			
		||||
      $this->drupalGet($translations_url);
 | 
			
		||||
      $editor = $expected_status['delete'] == 200;
 | 
			
		||||
 | 
			
		||||
      if ($editor) {
 | 
			
		||||
        $this->clickLink('Delete', 1);
 | 
			
		||||
        // An editor should be pointed to the entity deletion form in
 | 
			
		||||
        // multilingual mode. We need a new expected delete path with a new
 | 
			
		||||
        // language.
 | 
			
		||||
        $expected_delete_path = $this->entity->toUrl('delete-form', $options)->toString();
 | 
			
		||||
        $this->assertSession()->addressEquals($expected_delete_path);
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        $this->clickLink('Delete');
 | 
			
		||||
        // While a translator should be pointed to the translation deletion
 | 
			
		||||
        // form.
 | 
			
		||||
        $this->assertSession()->addressEquals($delete_translation_url);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $this->drupalGet($delete_translation_url);
 | 
			
		||||
    }
 | 
			
		||||
    $this->assertSession()->statusCodeEquals($expected_status['delete_translation']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Assert that the current page does not contain shared form elements.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertNoSharedElements(): void {
 | 
			
		||||
    $language_none = LanguageInterface::LANGCODE_NOT_SPECIFIED;
 | 
			
		||||
    $this->assertSession()->fieldNotExists("field_test_text[$language_none][0][value]");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,14 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generic module test for content_translation.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class GenericTest extends GenericModuleTestBase {}
 | 
			
		||||
@ -0,0 +1,41 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional\Views;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\views_ui\Functional\UITestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the views UI when content_translation is enabled.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationViewsUITest extends UITestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Views used by this test.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  public static $testViews = ['test_view'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['content_translation'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the views UI.
 | 
			
		||||
   */
 | 
			
		||||
  public function testViewsUI(): void {
 | 
			
		||||
    $this->drupalGet('admin/structure/views/view/test_view/edit');
 | 
			
		||||
    $this->assertSession()->titleEquals('Test view (Views test data) | Drupal');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,78 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Functional\Views;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\content_translation\Functional\ContentTranslationTestBase;
 | 
			
		||||
use Drupal\views\Tests\ViewTestData;
 | 
			
		||||
use Drupal\Core\Language\Language;
 | 
			
		||||
use Drupal\user\Entity\User;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the content translation overview link field handler.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 * @see \Drupal\content_translation\Plugin\views\field\TranslationLink
 | 
			
		||||
 */
 | 
			
		||||
class TranslationLinkTest extends ContentTranslationTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Views used by this test.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  public static $testViews = ['test_entity_translations_link'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['content_translation_test_views'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    // @todo Use entity_type once it has multilingual Views integration.
 | 
			
		||||
    $this->entityTypeId = 'user';
 | 
			
		||||
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->doSetup();
 | 
			
		||||
 | 
			
		||||
    // Assign user 1  a language code so that the entity can be translated.
 | 
			
		||||
    $user = User::load(1);
 | 
			
		||||
    $user->langcode = 'en';
 | 
			
		||||
    $user->save();
 | 
			
		||||
 | 
			
		||||
    // Assign user 2 LANGCODE_NOT_SPECIFIED code so entity can't be translated.
 | 
			
		||||
    $user = User::load(2);
 | 
			
		||||
    $user->langcode = Language::LANGCODE_NOT_SPECIFIED;
 | 
			
		||||
    $user->save();
 | 
			
		||||
 | 
			
		||||
    ViewTestData::createTestViews(static::class, ['content_translation_test_views']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getTranslatorPermissions() {
 | 
			
		||||
    $permissions = parent::getTranslatorPermissions();
 | 
			
		||||
    $permissions[] = 'access user profiles';
 | 
			
		||||
    return $permissions;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the content translation overview link field handler.
 | 
			
		||||
   */
 | 
			
		||||
  public function testTranslationLink(): void {
 | 
			
		||||
    $this->drupalGet('test-entity-translations-link');
 | 
			
		||||
    $this->assertSession()->linkByHrefExists('user/1/translations');
 | 
			
		||||
    $this->assertSession()->linkByHrefNotExists('user/2/translations', 'The translations link is not present when content_translation_translate_access() is FALSE.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,47 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\FunctionalJavascript;
 | 
			
		||||
 | 
			
		||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests that the content translation configuration javascript does't fail.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationConfigUITest extends WebDriverTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['content_translation', 'node'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Use the minimal profile.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'standard';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that the content translation configuration javascript does't fail.
 | 
			
		||||
   */
 | 
			
		||||
  public function testContentTranslationConfigUI(): void {
 | 
			
		||||
    $content_translation_manager = $this->container->get('content_translation.manager');
 | 
			
		||||
    $content_translation_manager->setEnabled('node', 'article', TRUE);
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
 | 
			
		||||
    $admin = $this->drupalCreateUser([], NULL, TRUE);
 | 
			
		||||
    $this->drupalLogin($admin);
 | 
			
		||||
    $this->drupalGet('/admin/config/regional/content-language');
 | 
			
		||||
    $this->failOnJavaScriptErrors();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,74 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\FunctionalJavascript;
 | 
			
		||||
 | 
			
		||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests that contextual links are available for content translation.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationContextualLinksTest extends WebDriverTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The 'translator' user to use during testing.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\user\UserInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $translator;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['content_translation', 'contextual', 'node'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    // Set up an additional language.
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('es')->save();
 | 
			
		||||
 | 
			
		||||
    // Create a content type.
 | 
			
		||||
    $this->drupalCreateContentType(['type' => 'page']);
 | 
			
		||||
 | 
			
		||||
    // Enable content translation.
 | 
			
		||||
    $content_translation_manager = $this->container->get('content_translation.manager');
 | 
			
		||||
    $content_translation_manager->setEnabled('node', 'page', TRUE);
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
 | 
			
		||||
    // Create a translator user.
 | 
			
		||||
    $permissions = [
 | 
			
		||||
      'access contextual links',
 | 
			
		||||
      'administer nodes',
 | 
			
		||||
      'edit any page content',
 | 
			
		||||
      'translate any entity',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->translator = $this->drupalCreateUser($permissions);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that a contextual link is available for translating a node.
 | 
			
		||||
   */
 | 
			
		||||
  public function testContentTranslationContextualLinks(): void {
 | 
			
		||||
    $node = $this->drupalCreateNode(['type' => 'page', 'title' => 'Test']);
 | 
			
		||||
 | 
			
		||||
    // Check that the translate link appears on the node page.
 | 
			
		||||
    $this->drupalLogin($this->translator);
 | 
			
		||||
    $this->drupalGet('node/' . $node->id());
 | 
			
		||||
    $link = $this->assertSession()->waitForElement('css', '[data-contextual-id^="node:node=1"] .contextual-links a:contains("Translate")');
 | 
			
		||||
    $this->assertStringContainsString('node/1/translations', $link->getAttribute('href'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,114 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Config\ConfigImporter;
 | 
			
		||||
use Drupal\Core\Config\StorageComparer;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests content translation updates performed during config import.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationConfigImportTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Config Importer object used for testing.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Config\ConfigImporter
 | 
			
		||||
   */
 | 
			
		||||
  protected $configImporter;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'system',
 | 
			
		||||
    'user',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installConfig(['system']);
 | 
			
		||||
    $this->installEntitySchema('entity_test_mul');
 | 
			
		||||
    $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
 | 
			
		||||
 | 
			
		||||
    // Set up the ConfigImporter object for testing.
 | 
			
		||||
    $storage_comparer = new StorageComparer(
 | 
			
		||||
      $this->container->get('config.storage.sync'),
 | 
			
		||||
      $this->container->get('config.storage')
 | 
			
		||||
    );
 | 
			
		||||
    $this->configImporter = new ConfigImporter(
 | 
			
		||||
      $storage_comparer->createChangelist(),
 | 
			
		||||
      $this->container->get('event_dispatcher'),
 | 
			
		||||
      $this->container->get('config.manager'),
 | 
			
		||||
      $this->container->get('lock'),
 | 
			
		||||
      $this->container->get('config.typed'),
 | 
			
		||||
      $this->container->get('module_handler'),
 | 
			
		||||
      $this->container->get('module_installer'),
 | 
			
		||||
      $this->container->get('theme_handler'),
 | 
			
		||||
      $this->container->get('string_translation'),
 | 
			
		||||
      $this->container->get('extension.list.module'),
 | 
			
		||||
      $this->container->get('extension.list.theme')
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests config import updates.
 | 
			
		||||
   */
 | 
			
		||||
  public function testConfigImportUpdates(): void {
 | 
			
		||||
    $entity_type_id = 'entity_test_mul';
 | 
			
		||||
    $config_id = $entity_type_id . '.' . $entity_type_id;
 | 
			
		||||
    $config_name = 'language.content_settings.' . $config_id;
 | 
			
		||||
    $storage = $this->container->get('config.storage');
 | 
			
		||||
    $sync = $this->container->get('config.storage.sync');
 | 
			
		||||
 | 
			
		||||
    // Verify the configuration to create does not exist yet.
 | 
			
		||||
    $this->assertFalse($storage->exists($config_name), $config_name . ' not found.');
 | 
			
		||||
 | 
			
		||||
    // Create new config entity.
 | 
			
		||||
    $data = [
 | 
			
		||||
      'uuid' => 'a019d89b-c4d9-4ed4-b859-894e4e2e93cf',
 | 
			
		||||
      'langcode' => 'en',
 | 
			
		||||
      'status' => TRUE,
 | 
			
		||||
      'dependencies' => [
 | 
			
		||||
        'module' => ['content_translation'],
 | 
			
		||||
      ],
 | 
			
		||||
      'id' => $config_id,
 | 
			
		||||
      'target_entity_type_id' => 'entity_test_mul',
 | 
			
		||||
      'target_bundle' => 'entity_test_mul',
 | 
			
		||||
      'default_langcode' => 'site_default',
 | 
			
		||||
      'language_alterable' => FALSE,
 | 
			
		||||
      'third_party_settings' => [
 | 
			
		||||
        'content_translation' => ['enabled' => TRUE],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
    $sync->write($config_name, $data);
 | 
			
		||||
    $this->assertTrue($sync->exists($config_name), $config_name . ' found.');
 | 
			
		||||
 | 
			
		||||
    // Import.
 | 
			
		||||
    $this->configImporter->reset()->import();
 | 
			
		||||
 | 
			
		||||
    // Verify the values appeared.
 | 
			
		||||
    $config = $this->config($config_name);
 | 
			
		||||
    $this->assertSame($config_id, $config->get('id'));
 | 
			
		||||
 | 
			
		||||
    // Verify that updates were performed.
 | 
			
		||||
    $entity_type = $this->container->get('entity_type.manager')->getDefinition($entity_type_id);
 | 
			
		||||
    $table = $entity_type->getDataTable();
 | 
			
		||||
    $db_schema = $this->container->get('database')->schema();
 | 
			
		||||
    $result = $db_schema->fieldExists($table, 'content_translation_source') && $db_schema->fieldExists($table, 'content_translation_outdated');
 | 
			
		||||
    $this->assertTrue($result, 'Content translation updates were successfully performed during config import.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,153 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestMul;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the Content Translation bundle info logic.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationEntityBundleInfoTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'system',
 | 
			
		||||
    'node',
 | 
			
		||||
    'user',
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation_test',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The content translation manager.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\content_translation\ContentTranslationManagerInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $contentTranslationManager;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The bundle info service.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Entity\EntityTypeBundleInfo
 | 
			
		||||
   */
 | 
			
		||||
  protected $bundleInfo;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->contentTranslationManager = $this->container->get('content_translation.manager');
 | 
			
		||||
    $this->bundleInfo = $this->container->get('entity_type.bundle.info');
 | 
			
		||||
 | 
			
		||||
    $this->installEntitySchema('entity_test_mul');
 | 
			
		||||
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('it')->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that modules can know whether bundles are translatable.
 | 
			
		||||
   */
 | 
			
		||||
  public function testHookInvocationOrder(): void {
 | 
			
		||||
    $this->contentTranslationManager->setEnabled('entity_test_mul', 'entity_test_mul', TRUE);
 | 
			
		||||
    $this->bundleInfo->clearCachedBundles();
 | 
			
		||||
    $this->bundleInfo->getAllBundleInfo();
 | 
			
		||||
 | 
			
		||||
    // Verify that the test module comes first in the module list, which would
 | 
			
		||||
    // normally make its hook implementation to be invoked first.
 | 
			
		||||
    /** @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */
 | 
			
		||||
    $module_handler = $this->container->get('module_handler');
 | 
			
		||||
    $module_list = $module_handler->getModuleList();
 | 
			
		||||
    $expected_modules = [
 | 
			
		||||
      'content_translation_test',
 | 
			
		||||
      'content_translation',
 | 
			
		||||
    ];
 | 
			
		||||
    $actual_modules = array_keys(array_intersect_key($module_list, array_flip($expected_modules)));
 | 
			
		||||
    $this->assertEquals($expected_modules, $actual_modules);
 | 
			
		||||
 | 
			
		||||
    // Check that the "content_translation_test" hook implementation has access
 | 
			
		||||
    // to the "translatable" bundle info property.
 | 
			
		||||
    /** @var \Drupal\Core\State\StateInterface $state */
 | 
			
		||||
    $state = $this->container->get('state');
 | 
			
		||||
    $this->assertTrue($state->get('content_translation_test.translatable'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that field synchronization is skipped for disabled bundles.
 | 
			
		||||
   */
 | 
			
		||||
  public function testFieldSynchronizationWithDisabledBundle(): void {
 | 
			
		||||
    $entity = EntityTestMul::create();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $translation */
 | 
			
		||||
    $translation = $entity->addTranslation('it');
 | 
			
		||||
    $translation->save();
 | 
			
		||||
 | 
			
		||||
    $this->assertTrue($entity->isTranslatable());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that bundle translation settings are propagated on creation.
 | 
			
		||||
   *
 | 
			
		||||
   * @throws \Drupal\Core\Entity\EntityStorageException
 | 
			
		||||
   */
 | 
			
		||||
  public function testBundleClearOnLanguageContentSettingInsert(): void {
 | 
			
		||||
    $node = $this->getBundledNode();
 | 
			
		||||
    $this->assertFalse($node->isTranslatable());
 | 
			
		||||
    $this->contentTranslationManager->setEnabled('node', 'bundle_test', TRUE);
 | 
			
		||||
    $this->assertTrue($node->isTranslatable(), "Bundle info was not cleared on language_content_settings creation.");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that bundle translation setting changes are propagated.
 | 
			
		||||
   *
 | 
			
		||||
   * @throws \Drupal\Core\Entity\EntityStorageException
 | 
			
		||||
   * @throws \Exception
 | 
			
		||||
   */
 | 
			
		||||
  public function testBundleClearOnLanguageContentSettingUpdate(): void {
 | 
			
		||||
    $node = $this->getBundledNode();
 | 
			
		||||
    $this->assertFalse($node->isTranslatable());
 | 
			
		||||
    $this->container->get('entity_type.manager')->getStorage('language_content_settings')->create([
 | 
			
		||||
      'target_entity_type_id' => 'node',
 | 
			
		||||
      'target_bundle' => 'bundle_test',
 | 
			
		||||
    ])->save();
 | 
			
		||||
    $this->assertFalse($node->isTranslatable());
 | 
			
		||||
    $this->contentTranslationManager->setEnabled('node', 'bundle_test', TRUE);
 | 
			
		||||
    $this->assertTrue($node->isTranslatable(), "Bundle info was not cleared on language_content_settings update.");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets a new bundled node for testing.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\node\Entity\Node
 | 
			
		||||
   *   The new node.
 | 
			
		||||
   *
 | 
			
		||||
   * @throws \Drupal\Core\Entity\EntityStorageException
 | 
			
		||||
   */
 | 
			
		||||
  protected function getBundledNode() {
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
    $bundle = NodeType::create([
 | 
			
		||||
      'type' => 'bundle_test',
 | 
			
		||||
      'name' => 'Bundle Test',
 | 
			
		||||
    ]);
 | 
			
		||||
    $bundle->save();
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'type' => 'bundle_test',
 | 
			
		||||
    ]);
 | 
			
		||||
    return $node;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,581 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\ContentEntityInterface;
 | 
			
		||||
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
 | 
			
		||||
use Drupal\Core\Language\LanguageInterface;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestMulRev;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\file\Entity\File;
 | 
			
		||||
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\Tests\TestFileCreationTrait;
 | 
			
		||||
use Drupal\user\Entity\User;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the field synchronization logic when revisions are involved.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationFieldSyncRevisionTest extends EntityKernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use TestFileCreationTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'file',
 | 
			
		||||
    'image',
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'content_translation_test',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The synchronized field name.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldName = 'sync_field';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The content translation manager.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\content_translation\ContentTranslationManagerInterface|\Drupal\content_translation\BundleTranslationSettingsInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $contentTranslationManager;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The test entity storage.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Entity\ContentEntityStorageInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $storage;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $entity_type_id = 'entity_test_mulrev';
 | 
			
		||||
    $this->installEntitySchema($entity_type_id);
 | 
			
		||||
    $this->installEntitySchema('file');
 | 
			
		||||
    $this->installSchema('file', ['file_usage']);
 | 
			
		||||
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('it')->save();
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('fr')->save();
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\field\Entity\FieldStorageConfig $field_storage */
 | 
			
		||||
    $field_storage_config = FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $this->fieldName,
 | 
			
		||||
      'type' => 'image',
 | 
			
		||||
      'entity_type' => $entity_type_id,
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
      'translatable' => 1,
 | 
			
		||||
    ]);
 | 
			
		||||
    $field_storage_config->save();
 | 
			
		||||
 | 
			
		||||
    $field_config = FieldConfig::create([
 | 
			
		||||
      'entity_type' => $entity_type_id,
 | 
			
		||||
      'field_name' => $this->fieldName,
 | 
			
		||||
      'bundle' => $entity_type_id,
 | 
			
		||||
      'label' => 'Synchronized field',
 | 
			
		||||
      'translatable' => 1,
 | 
			
		||||
    ]);
 | 
			
		||||
    $field_config->save();
 | 
			
		||||
 | 
			
		||||
    $property_settings = [
 | 
			
		||||
      'alt' => 'alt',
 | 
			
		||||
      'title' => 'title',
 | 
			
		||||
      'file' => 0,
 | 
			
		||||
    ];
 | 
			
		||||
    $field_config->setThirdPartySetting('content_translation', 'translation_sync', $property_settings);
 | 
			
		||||
    $field_config->save();
 | 
			
		||||
 | 
			
		||||
    $this->contentTranslationManager = $this->container->get('content_translation.manager');
 | 
			
		||||
    $this->contentTranslationManager->setEnabled($entity_type_id, $entity_type_id, TRUE);
 | 
			
		||||
 | 
			
		||||
    $this->storage = $this->entityTypeManager->getStorage($entity_type_id);
 | 
			
		||||
 | 
			
		||||
    foreach ($this->getTestFiles('image') as $file) {
 | 
			
		||||
      $entity = File::create((array) $file + ['status' => 1]);
 | 
			
		||||
      $entity->save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $this->state->set('content_translation.entity_access.file', ['view' => TRUE]);
 | 
			
		||||
 | 
			
		||||
    $account = User::create([
 | 
			
		||||
      'name' => $this->randomMachineName(),
 | 
			
		||||
      'status' => 1,
 | 
			
		||||
    ]);
 | 
			
		||||
    $account->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Checks that field synchronization works as expected with revisions.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::create
 | 
			
		||||
   * @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::validate
 | 
			
		||||
   * @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::hasSynchronizedPropertyChanges
 | 
			
		||||
   * @covers \Drupal\content_translation\FieldTranslationSynchronizer::getFieldSynchronizedProperties
 | 
			
		||||
   * @covers \Drupal\content_translation\FieldTranslationSynchronizer::synchronizeFields
 | 
			
		||||
   * @covers \Drupal\content_translation\FieldTranslationSynchronizer::synchronizeItems
 | 
			
		||||
   */
 | 
			
		||||
  public function testFieldSynchronizationAndValidation(): void {
 | 
			
		||||
    // Test that when untranslatable field widgets are displayed, synchronized
 | 
			
		||||
    // field properties can be changed only in default revisions.
 | 
			
		||||
    $this->setUntranslatableFieldWidgetsDisplay(TRUE);
 | 
			
		||||
    $entity = $this->saveNewEntity();
 | 
			
		||||
    $entity_id = $entity->id();
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [1, 1, 1, 'Alt 1 EN']);
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
 | 
			
		||||
    $en_revision = $this->createRevision($entity, FALSE);
 | 
			
		||||
    $en_revision->get($this->fieldName)->target_id = 2;
 | 
			
		||||
    $violations = $en_revision->validate();
 | 
			
		||||
    $this->assertViolations($violations);
 | 
			
		||||
 | 
			
		||||
    $it_translation = $entity->addTranslation('it', $entity->toArray());
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
 | 
			
		||||
    $it_revision = $this->createRevision($it_translation, FALSE);
 | 
			
		||||
    $metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
 | 
			
		||||
    $metadata->setSource('en');
 | 
			
		||||
    $it_revision->get($this->fieldName)->target_id = 2;
 | 
			
		||||
    $it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
 | 
			
		||||
    $violations = $it_revision->validate();
 | 
			
		||||
    $this->assertViolations($violations);
 | 
			
		||||
    $it_revision->isDefaultRevision(TRUE);
 | 
			
		||||
    $violations = $it_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($it_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [2, 2, 2, 'Alt 1 EN', 'Alt 2 IT']);
 | 
			
		||||
 | 
			
		||||
    $en_revision = $this->createRevision($en_revision, FALSE);
 | 
			
		||||
    $en_revision->get($this->fieldName)->alt = 'Alt 3 EN';
 | 
			
		||||
    $violations = $en_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($en_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [3, 2, 2, 'Alt 3 EN', 'Alt 2 IT']);
 | 
			
		||||
 | 
			
		||||
    $it_revision = $this->createRevision($it_revision, FALSE);
 | 
			
		||||
    $it_revision->get($this->fieldName)->alt = 'Alt 4 IT';
 | 
			
		||||
    $violations = $it_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($it_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [4, 2, 2, 'Alt 1 EN', 'Alt 4 IT']);
 | 
			
		||||
 | 
			
		||||
    $en_revision = $this->createRevision($en_revision);
 | 
			
		||||
    $en_revision->get($this->fieldName)->alt = 'Alt 5 EN';
 | 
			
		||||
    $violations = $en_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($en_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [5, 2, 2, 'Alt 5 EN', 'Alt 2 IT']);
 | 
			
		||||
 | 
			
		||||
    $en_revision = $this->createRevision($en_revision);
 | 
			
		||||
    $en_revision->get($this->fieldName)->target_id = 6;
 | 
			
		||||
    $en_revision->get($this->fieldName)->alt = 'Alt 6 EN';
 | 
			
		||||
    $violations = $en_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($en_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [6, 6, 6, 'Alt 6 EN', 'Alt 2 IT']);
 | 
			
		||||
 | 
			
		||||
    $it_revision = $this->createRevision($it_revision);
 | 
			
		||||
    $it_revision->get($this->fieldName)->alt = 'Alt 7 IT';
 | 
			
		||||
    $violations = $it_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($it_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [7, 6, 6, 'Alt 6 EN', 'Alt 7 IT']);
 | 
			
		||||
 | 
			
		||||
    // Test that when untranslatable field widgets are hidden, synchronized
 | 
			
		||||
    // field properties can be changed only when editing the default
 | 
			
		||||
    // translation. This may lead to temporarily desynchronized values, when
 | 
			
		||||
    // saving a pending revision for the default translation that changes a
 | 
			
		||||
    // synchronized property (see revision 11).
 | 
			
		||||
    $this->setUntranslatableFieldWidgetsDisplay(FALSE);
 | 
			
		||||
    $entity = $this->saveNewEntity();
 | 
			
		||||
    $entity_id = $entity->id();
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [8, 1, 1, 'Alt 1 EN']);
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
 | 
			
		||||
    $en_revision = $this->createRevision($entity, FALSE);
 | 
			
		||||
    $en_revision->get($this->fieldName)->target_id = 2;
 | 
			
		||||
    $en_revision->get($this->fieldName)->alt = 'Alt 2 EN';
 | 
			
		||||
    $violations = $en_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($en_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [9, 2, 2, 'Alt 2 EN']);
 | 
			
		||||
 | 
			
		||||
    $it_translation = $entity->addTranslation('it', $entity->toArray());
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
 | 
			
		||||
    $it_revision = $this->createRevision($it_translation, FALSE);
 | 
			
		||||
    $metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
 | 
			
		||||
    $metadata->setSource('en');
 | 
			
		||||
    $it_revision->get($this->fieldName)->target_id = 3;
 | 
			
		||||
    $violations = $it_revision->validate();
 | 
			
		||||
    $this->assertViolations($violations);
 | 
			
		||||
    $it_revision->isDefaultRevision(TRUE);
 | 
			
		||||
    $violations = $it_revision->validate();
 | 
			
		||||
    $this->assertViolations($violations);
 | 
			
		||||
 | 
			
		||||
    $it_revision = $this->createRevision($it_translation);
 | 
			
		||||
    $metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
 | 
			
		||||
    $metadata->setSource('en');
 | 
			
		||||
    $it_revision->get($this->fieldName)->alt = 'Alt 3 IT';
 | 
			
		||||
    $violations = $it_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($it_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [10, 1, 1, 'Alt 1 EN', 'Alt 3 IT']);
 | 
			
		||||
 | 
			
		||||
    $en_revision = $this->createRevision($en_revision, FALSE);
 | 
			
		||||
    $en_revision->get($this->fieldName)->alt = 'Alt 4 EN';
 | 
			
		||||
    $violations = $en_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($en_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [11, 2, 1, 'Alt 4 EN', 'Alt 3 IT']);
 | 
			
		||||
 | 
			
		||||
    $it_revision = $this->createRevision($it_revision, FALSE);
 | 
			
		||||
    $it_revision->get($this->fieldName)->alt = 'Alt 5 IT';
 | 
			
		||||
    $violations = $it_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($it_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [12, 1, 1, 'Alt 1 EN', 'Alt 5 IT']);
 | 
			
		||||
 | 
			
		||||
    $en_revision = $this->createRevision($en_revision);
 | 
			
		||||
    $en_revision->get($this->fieldName)->target_id = 6;
 | 
			
		||||
    $en_revision->get($this->fieldName)->alt = 'Alt 6 EN';
 | 
			
		||||
    $violations = $en_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($en_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [13, 6, 6, 'Alt 6 EN', 'Alt 3 IT']);
 | 
			
		||||
 | 
			
		||||
    $it_revision = $this->createRevision($it_revision);
 | 
			
		||||
    $it_revision->get($this->fieldName)->target_id = 7;
 | 
			
		||||
    $violations = $it_revision->validate();
 | 
			
		||||
    $this->assertViolations($violations);
 | 
			
		||||
 | 
			
		||||
    $it_revision = $this->createRevision($it_revision);
 | 
			
		||||
    $it_revision->get($this->fieldName)->alt = 'Alt 7 IT';
 | 
			
		||||
    $violations = $it_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($it_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [14, 6, 6, 'Alt 6 EN', 'Alt 7 IT']);
 | 
			
		||||
 | 
			
		||||
    // Test that creating a default revision starting from a pending revision
 | 
			
		||||
    // having changes to synchronized properties, without introducing new
 | 
			
		||||
    // changes works properly.
 | 
			
		||||
    $this->setUntranslatableFieldWidgetsDisplay(FALSE);
 | 
			
		||||
    $entity = $this->saveNewEntity();
 | 
			
		||||
    $entity_id = $entity->id();
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [15, 1, 1, 'Alt 1 EN']);
 | 
			
		||||
 | 
			
		||||
    $it_translation = $entity->addTranslation('it', $entity->toArray());
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
 | 
			
		||||
    $it_revision = $this->createRevision($it_translation);
 | 
			
		||||
    $metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
 | 
			
		||||
    $metadata->setSource('en');
 | 
			
		||||
    $it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
 | 
			
		||||
    $violations = $it_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($it_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [16, 1, 1, 'Alt 1 EN', 'Alt 2 IT']);
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
 | 
			
		||||
    $en_revision = $this->createRevision($entity);
 | 
			
		||||
    $en_revision->get($this->fieldName)->target_id = 3;
 | 
			
		||||
    $en_revision->get($this->fieldName)->alt = 'Alt 3 EN';
 | 
			
		||||
    $violations = $en_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($en_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [17, 3, 3, 'Alt 3 EN', 'Alt 2 IT']);
 | 
			
		||||
 | 
			
		||||
    $en_revision = $this->createRevision($entity, FALSE);
 | 
			
		||||
    $en_revision->get($this->fieldName)->target_id = 4;
 | 
			
		||||
    $en_revision->get($this->fieldName)->alt = 'Alt 4 EN';
 | 
			
		||||
    $violations = $en_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($en_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [18, 4, 3, 'Alt 4 EN', 'Alt 2 IT']);
 | 
			
		||||
 | 
			
		||||
    $en_revision = $this->createRevision($entity);
 | 
			
		||||
    $violations = $en_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($en_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [19, 4, 4, 'Alt 4 EN', 'Alt 2 IT']);
 | 
			
		||||
 | 
			
		||||
    $it_revision = $this->createRevision($it_revision);
 | 
			
		||||
    $it_revision->get($this->fieldName)->alt = 'Alt 6 IT';
 | 
			
		||||
    $violations = $it_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($it_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [20, 4, 4, 'Alt 4 EN', 'Alt 6 IT']);
 | 
			
		||||
 | 
			
		||||
    // Check that we are not allowed to perform changes to multiple translations
 | 
			
		||||
    // in pending revisions when synchronized properties are involved.
 | 
			
		||||
    $this->setUntranslatableFieldWidgetsDisplay(FALSE);
 | 
			
		||||
    $entity = $this->saveNewEntity();
 | 
			
		||||
    $entity_id = $entity->id();
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [21, 1, 1, 'Alt 1 EN']);
 | 
			
		||||
 | 
			
		||||
    $it_translation = $entity->addTranslation('it', $entity->toArray());
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
 | 
			
		||||
    $it_revision = $this->createRevision($it_translation);
 | 
			
		||||
    $metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
 | 
			
		||||
    $metadata->setSource('en');
 | 
			
		||||
    $it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
 | 
			
		||||
    $violations = $it_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($it_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [22, 1, 1, 'Alt 1 EN', 'Alt 2 IT']);
 | 
			
		||||
 | 
			
		||||
    $en_revision = $this->createRevision($entity, FALSE);
 | 
			
		||||
    $en_revision->get($this->fieldName)->target_id = 2;
 | 
			
		||||
    $en_revision->getTranslation('it')->get($this->fieldName)->alt = 'Alt 3 IT';
 | 
			
		||||
    $violations = $en_revision->validate();
 | 
			
		||||
    $this->assertViolations($violations);
 | 
			
		||||
 | 
			
		||||
    // Test that when saving a new default revision starting from a pending
 | 
			
		||||
    // revision, outdated synchronized properties do not override more recent
 | 
			
		||||
    // ones.
 | 
			
		||||
    $this->setUntranslatableFieldWidgetsDisplay(TRUE);
 | 
			
		||||
    $entity = $this->saveNewEntity();
 | 
			
		||||
    $entity_id = $entity->id();
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [23, 1, 1, 'Alt 1 EN']);
 | 
			
		||||
 | 
			
		||||
    $it_translation = $entity->addTranslation('it', $entity->toArray());
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
 | 
			
		||||
    $it_revision = $this->createRevision($it_translation, FALSE);
 | 
			
		||||
    $metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
 | 
			
		||||
    $metadata->setSource('en');
 | 
			
		||||
    $it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
 | 
			
		||||
    $violations = $it_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($it_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [24, 1, 1, 'Alt 1 EN', 'Alt 2 IT']);
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
 | 
			
		||||
    $en_revision = $this->createRevision($entity);
 | 
			
		||||
    $en_revision->get($this->fieldName)->target_id = 3;
 | 
			
		||||
    $en_revision->get($this->fieldName)->alt = 'Alt 3 EN';
 | 
			
		||||
    $violations = $en_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($en_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [25, 3, 3, 'Alt 3 EN', 'Alt 2 IT']);
 | 
			
		||||
 | 
			
		||||
    $it_revision = $this->createRevision($it_revision);
 | 
			
		||||
    $it_revision->get($this->fieldName)->alt = 'Alt 4 IT';
 | 
			
		||||
    $violations = $it_revision->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($it_revision);
 | 
			
		||||
    $this->assertLatestRevisionFieldValues($entity_id, [26, 3, 3, 'Alt 3 EN', 'Alt 4 IT']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Checks that file field synchronization works as expected.
 | 
			
		||||
   */
 | 
			
		||||
  public function testFileFieldSynchronization(): void {
 | 
			
		||||
    $entity_type_id = 'entity_test_mulrev';
 | 
			
		||||
    $file_field_name = 'file_field';
 | 
			
		||||
 | 
			
		||||
    foreach ($this->getTestFiles('text') as $file) {
 | 
			
		||||
      $entity = File::create((array) $file + ['status' => 1]);
 | 
			
		||||
      $entity->save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\field\Entity\FieldStorageConfig $field_storage */
 | 
			
		||||
    $field_storage_config = FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $file_field_name,
 | 
			
		||||
      'type' => 'file',
 | 
			
		||||
      'entity_type' => $entity_type_id,
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
      'translatable' => 1,
 | 
			
		||||
    ]);
 | 
			
		||||
    $field_storage_config->save();
 | 
			
		||||
 | 
			
		||||
    $field_config = FieldConfig::create([
 | 
			
		||||
      'entity_type' => $entity_type_id,
 | 
			
		||||
      'field_name' => $file_field_name,
 | 
			
		||||
      'bundle' => $entity_type_id,
 | 
			
		||||
      'label' => 'Synchronized file field',
 | 
			
		||||
      'translatable' => 1,
 | 
			
		||||
    ]);
 | 
			
		||||
    $field_config->save();
 | 
			
		||||
 | 
			
		||||
    $property_settings = [
 | 
			
		||||
      'display' => 'display',
 | 
			
		||||
      'description' => 'description',
 | 
			
		||||
      'target_id' => 0,
 | 
			
		||||
    ];
 | 
			
		||||
    $field_config->setThirdPartySetting('content_translation', 'translation_sync', $property_settings);
 | 
			
		||||
    $field_config->save();
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
 | 
			
		||||
    $entity = EntityTestMulRev::create([
 | 
			
		||||
      'uid' => 1,
 | 
			
		||||
      'langcode' => 'en',
 | 
			
		||||
      $file_field_name => [
 | 
			
		||||
        'target_id' => 1,
 | 
			
		||||
        'description' => 'Description EN',
 | 
			
		||||
        'display' => 1,
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals(1, $entity->get($file_field_name)->target_id);
 | 
			
		||||
    $this->assertEquals('Description EN', $entity->get($file_field_name)->description);
 | 
			
		||||
    $this->assertEquals(1, $entity->get($file_field_name)->display);
 | 
			
		||||
 | 
			
		||||
    // Create a translation with a different file, description and display
 | 
			
		||||
    // values.
 | 
			
		||||
    $it_translation = $entity->addTranslation('it', $entity->toArray());
 | 
			
		||||
    $it_translation->get($file_field_name)->target_id = 2;
 | 
			
		||||
    $it_translation->get($file_field_name)->description = 'Description IT';
 | 
			
		||||
    $it_translation->get($file_field_name)->display = 0;
 | 
			
		||||
    $metadata = $this->contentTranslationManager->getTranslationMetadata($it_translation);
 | 
			
		||||
    $metadata->setSource('en');
 | 
			
		||||
    $it_translation->save();
 | 
			
		||||
 | 
			
		||||
    $it_entity = $entity->getTranslation('it');
 | 
			
		||||
    $this->assertEquals(2, $it_entity->get($file_field_name)->target_id);
 | 
			
		||||
    $this->assertEquals('Description IT', $it_entity->get($file_field_name)->description);
 | 
			
		||||
    $this->assertEquals(0, $it_entity->get($file_field_name)->display);
 | 
			
		||||
 | 
			
		||||
    // In the english entity the file should have changed, but the description
 | 
			
		||||
    // and display should have remained the same.
 | 
			
		||||
    $en_entity = $entity->getTranslation('en');
 | 
			
		||||
    $this->assertEquals(2, $en_entity->get($file_field_name)->target_id);
 | 
			
		||||
    $this->assertEquals('Description EN', $en_entity->get($file_field_name)->description);
 | 
			
		||||
    $this->assertEquals(1, $en_entity->get($file_field_name)->display);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests changing the default language of an entity.
 | 
			
		||||
   */
 | 
			
		||||
  public function testChangeDefaultLanguageNonTranslatableFieldsHidden(): void {
 | 
			
		||||
    $this->setUntranslatableFieldWidgetsDisplay(FALSE);
 | 
			
		||||
    $entity = $this->saveNewEntity();
 | 
			
		||||
    $entity->langcode = 'it';
 | 
			
		||||
    $this->assertCount(0, $entity->validate());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Sets untranslatable field widgets' display status.
 | 
			
		||||
   *
 | 
			
		||||
   * @param bool $display
 | 
			
		||||
   *   Whether untranslatable field widgets should be displayed.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUntranslatableFieldWidgetsDisplay($display): void {
 | 
			
		||||
    $entity_type_id = $this->storage->getEntityTypeId();
 | 
			
		||||
    $settings = ['untranslatable_fields_hide' => !$display];
 | 
			
		||||
    $this->contentTranslationManager->setBundleTranslationSettings($entity_type_id, $entity_type_id, $settings);
 | 
			
		||||
    /** @var \Drupal\Core\Entity\EntityTypeBundleInfo $bundle_info */
 | 
			
		||||
    $bundle_info = $this->container->get('entity_type.bundle.info');
 | 
			
		||||
    $bundle_info->clearCachedBundles();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @return \Drupal\Core\Entity\ContentEntityInterface
 | 
			
		||||
   *   The saved entity.
 | 
			
		||||
   */
 | 
			
		||||
  protected function saveNewEntity() {
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
 | 
			
		||||
    $entity = EntityTestMulRev::create([
 | 
			
		||||
      'uid' => 1,
 | 
			
		||||
      'langcode' => 'en',
 | 
			
		||||
      $this->fieldName => [
 | 
			
		||||
        'target_id' => 1,
 | 
			
		||||
        'alt' => 'Alt 1 EN',
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
    $metadata = $this->contentTranslationManager->getTranslationMetadata($entity);
 | 
			
		||||
    $metadata->setSource(LanguageInterface::LANGCODE_NOT_SPECIFIED);
 | 
			
		||||
    $violations = $entity->validate();
 | 
			
		||||
    $this->assertEmpty($violations);
 | 
			
		||||
    $this->storage->save($entity);
 | 
			
		||||
    return $entity;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a new revision starting from the latest translation-affecting one.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Entity\ContentEntityInterface $translation
 | 
			
		||||
   *   The translation to be revisioned.
 | 
			
		||||
   * @param bool $default
 | 
			
		||||
   *   (optional) Whether the new revision should be marked as default. Defaults
 | 
			
		||||
   *   to TRUE.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Entity\ContentEntityInterface
 | 
			
		||||
   *   An entity revision object.
 | 
			
		||||
   */
 | 
			
		||||
  protected function createRevision(ContentEntityInterface $translation, $default = TRUE) {
 | 
			
		||||
    if (!$translation->isNewTranslation()) {
 | 
			
		||||
      $langcode = $translation->language()->getId();
 | 
			
		||||
      $revision_id = $this->storage->getLatestTranslationAffectedRevisionId($translation->id(), $langcode);
 | 
			
		||||
      /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
 | 
			
		||||
      $revision = $this->storage->loadRevision($revision_id);
 | 
			
		||||
      $translation = $revision->getTranslation($langcode);
 | 
			
		||||
    }
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
 | 
			
		||||
    $revision = $this->storage->createRevision($translation, $default);
 | 
			
		||||
    return $revision;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts that the expected violations were found.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations
 | 
			
		||||
   *   A list of violations.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertViolations(EntityConstraintViolationListInterface $violations): void {
 | 
			
		||||
    $entity_type_id = $this->storage->getEntityTypeId();
 | 
			
		||||
    $settings = $this->contentTranslationManager->getBundleTranslationSettings($entity_type_id, $entity_type_id);
 | 
			
		||||
    $message = !empty($settings['untranslatable_fields_hide']) ?
 | 
			
		||||
      'Non-translatable field elements can only be changed when updating the original language.' :
 | 
			
		||||
      'Non-translatable field elements can only be changed when updating the current revision.';
 | 
			
		||||
 | 
			
		||||
    $list = [];
 | 
			
		||||
    foreach ($violations as $violation) {
 | 
			
		||||
      if ((string) $violation->getMessage() === $message) {
 | 
			
		||||
        $list[] = $violation;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    $this->assertCount(1, $list);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts that the latest revision has the expected field values.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $entity_id
 | 
			
		||||
   *   The entity ID.
 | 
			
		||||
   * @param array $expected_values
 | 
			
		||||
   *   An array of expected values in the following order:
 | 
			
		||||
   *   - revision ID
 | 
			
		||||
   *   - target ID (en)
 | 
			
		||||
   *   - target ID (it)
 | 
			
		||||
   *   - alt (en)
 | 
			
		||||
   *   - alt (it)
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertLatestRevisionFieldValues(string $entity_id, array $expected_values): void {
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
 | 
			
		||||
    $entity = $this->storage->loadRevision($this->storage->getLatestRevisionId($entity_id));
 | 
			
		||||
    @[$revision_id, $target_id_en, $target_id_it, $alt_en, $alt_it] = $expected_values;
 | 
			
		||||
    $this->assertEquals($revision_id, $entity->getRevisionId());
 | 
			
		||||
    $this->assertEquals($target_id_en, $entity->get($this->fieldName)->target_id);
 | 
			
		||||
    $this->assertEquals($alt_en, $entity->get($this->fieldName)->alt);
 | 
			
		||||
    if ($entity->hasTranslation('it')) {
 | 
			
		||||
      $it_translation = $entity->getTranslation('it');
 | 
			
		||||
      $this->assertEquals($target_id_it, $it_translation->get($this->fieldName)->target_id);
 | 
			
		||||
      $this->assertEquals($alt_it, $it_translation->get($this->fieldName)->alt);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,235 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Form\FormState;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the content translation handler.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 *
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_translation\ContentTranslationHandler
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationHandlerTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'language',
 | 
			
		||||
    'user',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The state service.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\State\StateInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $state;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The entity type bundle information.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityTypeBundleInfo;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The entity type manager.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityTypeManager;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The messenger.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Messenger\MessengerInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $messenger;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The ID of the entity type used in this test.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityTypeId = 'entity_test_mul';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The ID of the translation language used in this test.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $translationLangcode = 'af';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->state = $this->container->get('state');
 | 
			
		||||
    $this->entityTypeBundleInfo = $this->container->get('entity_type.bundle.info');
 | 
			
		||||
    $this->entityTypeManager = $this->container->get('entity_type.manager');
 | 
			
		||||
    $this->messenger = $this->container->get('messenger');
 | 
			
		||||
 | 
			
		||||
    $this->installEntitySchema($this->entityTypeId);
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode($this->translationLangcode)->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests ContentTranslationHandler::entityFormSharedElements()
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $element
 | 
			
		||||
   *   The element that will be altered.
 | 
			
		||||
   * @param bool $default_translation_affected
 | 
			
		||||
   *   Whether or not only the default translation of the entity is affected.
 | 
			
		||||
   * @param bool $default_translation
 | 
			
		||||
   *   Whether or not the entity is the default translation.
 | 
			
		||||
   * @param bool $translation_form
 | 
			
		||||
   *   Whether or not the form is a translation form.
 | 
			
		||||
   * @param array $expected
 | 
			
		||||
   *   The expected altered element.
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider providerTestEntityFormSharedElements
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::entityFormSharedElements
 | 
			
		||||
   * @covers ::addTranslatabilityClue
 | 
			
		||||
   */
 | 
			
		||||
  public function testEntityFormSharedElements(array $element, $default_translation_affected, $default_translation, $translation_form, array $expected): void {
 | 
			
		||||
    $this->state->set('entity_test.translation', TRUE);
 | 
			
		||||
    $this->state->set('entity_test.untranslatable_fields.default_translation_affected', $default_translation_affected);
 | 
			
		||||
    $this->entityTypeBundleInfo->clearCachedBundles();
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
 | 
			
		||||
    $entity = $this->entityTypeManager->getStorage($this->entityTypeId)->create();
 | 
			
		||||
    if (!$default_translation) {
 | 
			
		||||
      $entity = $entity->addTranslation($this->translationLangcode);
 | 
			
		||||
    }
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $form_object = $this->entityTypeManager->getFormObject($this->entityTypeId, 'default');
 | 
			
		||||
    $form_object->setEntity($entity);
 | 
			
		||||
 | 
			
		||||
    $form_state = new FormState();
 | 
			
		||||
    $form_state
 | 
			
		||||
      ->addBuildInfo('callback_object', $form_object)
 | 
			
		||||
      ->set(['content_translation', 'translation_form'], $translation_form);
 | 
			
		||||
 | 
			
		||||
    $handler = $this->entityTypeManager->getHandler($this->entityTypeId, 'translation');
 | 
			
		||||
    $actual = $handler->entityFormSharedElements($element, $form_state, $element);
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals($expected, $actual);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns test cases for ::testEntityFormSharedElements().
 | 
			
		||||
   *
 | 
			
		||||
   * @return array[]
 | 
			
		||||
   *   An array of test cases, each one containing the element to alter, the
 | 
			
		||||
   *   form state, and the expected altered element.
 | 
			
		||||
   */
 | 
			
		||||
  public static function providerTestEntityFormSharedElements() {
 | 
			
		||||
    $tests = [];
 | 
			
		||||
 | 
			
		||||
    $element = [];
 | 
			
		||||
    $tests['empty'] = [
 | 
			
		||||
      'element' => $element,
 | 
			
		||||
      'default_translation_affected' => TRUE,
 | 
			
		||||
      'default_translation' => TRUE,
 | 
			
		||||
      'translation_form' => FALSE,
 | 
			
		||||
      'expected' => $element,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $element = [
 | 
			
		||||
      '#type' => 'textfield',
 | 
			
		||||
    ];
 | 
			
		||||
    $tests['no-children'] = $tests['empty'];
 | 
			
		||||
    $tests['no-children']['element'] = $element;
 | 
			
		||||
    $tests['no-children']['expected'] = $element;
 | 
			
		||||
 | 
			
		||||
    $element = [
 | 
			
		||||
      'test' => [
 | 
			
		||||
        '#type' => 'textfield',
 | 
			
		||||
        '#multilingual' => TRUE,
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
    $tests['multilingual'] = $tests['empty'];
 | 
			
		||||
    $tests['multilingual']['element'] = $element;
 | 
			
		||||
    $tests['multilingual']['expected'] = $element;
 | 
			
		||||
 | 
			
		||||
    unset($element['test']['#multilingual']);
 | 
			
		||||
    $tests['no-title'] = $tests['empty'];
 | 
			
		||||
    $tests['no-title']['element'] = $element;
 | 
			
		||||
    $tests['no-title']['expected'] = $element;
 | 
			
		||||
 | 
			
		||||
    $element['test']['#title'] = 'Test';
 | 
			
		||||
    $tests['no-translatability-clue'] = $tests['empty'];
 | 
			
		||||
    $tests['no-translatability-clue']['element'] = $element;
 | 
			
		||||
    $tests['no-translatability-clue']['expected'] = $element;
 | 
			
		||||
 | 
			
		||||
    $expected = $element;
 | 
			
		||||
    $expected['test']['#title'] .= ' <span class="translation-entity-all-languages">(all languages)</span>';
 | 
			
		||||
    $tests['translatability-clue'] = $tests['no-translatability-clue'];
 | 
			
		||||
    $tests['translatability-clue']['default_translation_affected'] = FALSE;
 | 
			
		||||
    $tests['translatability-clue']['expected'] = $expected;
 | 
			
		||||
 | 
			
		||||
    $ignored_types = [
 | 
			
		||||
      'actions',
 | 
			
		||||
      'details',
 | 
			
		||||
      'hidden',
 | 
			
		||||
      'link',
 | 
			
		||||
      'token',
 | 
			
		||||
      'value',
 | 
			
		||||
      'vertical_tabs',
 | 
			
		||||
    ];
 | 
			
		||||
    foreach ($ignored_types as $ignored_type) {
 | 
			
		||||
      $element = [
 | 
			
		||||
        'test' => [
 | 
			
		||||
          '#type' => $ignored_type,
 | 
			
		||||
          '#title' => 'Test',
 | 
			
		||||
        ],
 | 
			
		||||
      ];
 | 
			
		||||
      $tests["ignore-$ignored_type"] = $tests['translatability-clue'];
 | 
			
		||||
      $tests["ignore-$ignored_type"]['element'] = $element;
 | 
			
		||||
      $tests["ignore-$ignored_type"]['expected'] = $element;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $tests['unknown-field'] = $tests['no-translatability-clue'];
 | 
			
		||||
    $tests['unknown-field']['default_translation'] = FALSE;
 | 
			
		||||
 | 
			
		||||
    $element = [
 | 
			
		||||
      'name' => [
 | 
			
		||||
        '#type' => 'textfield',
 | 
			
		||||
      ],
 | 
			
		||||
      'hidden_fields_warning_message' => [
 | 
			
		||||
        '#theme' => 'status_messages',
 | 
			
		||||
        '#message_list' => [
 | 
			
		||||
          'warning' => [t('Fields that apply to all languages are hidden to avoid conflicting changes. <a href=":url">Edit them on the original language form</a>.')],
 | 
			
		||||
        ],
 | 
			
		||||
        '#weight' => -100,
 | 
			
		||||
        '#status_headings' => [
 | 
			
		||||
          'warning' => t('Warning message'),
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
    $expected = $element;
 | 
			
		||||
    $expected['name']['#access'] = FALSE;
 | 
			
		||||
    $tests['hide-untranslatable'] = $tests['unknown-field'];
 | 
			
		||||
    $tests['hide-untranslatable']['element'] = $element;
 | 
			
		||||
    $tests['hide-untranslatable']['expected'] = $expected;
 | 
			
		||||
 | 
			
		||||
    $tests['no-translation-form'] = $tests['no-translatability-clue'];
 | 
			
		||||
    $tests['no-translation-form']['translation_form'] = FALSE;
 | 
			
		||||
 | 
			
		||||
    return $tests;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,90 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestWithBundle;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests content translation for modules that provide translatable bundles.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationModuleInstallTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'content_translation_test',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'language',
 | 
			
		||||
    'user',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The content translation manager.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\content_translation\ContentTranslationManagerInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $contentTranslationManager;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The language code of the source language for this test.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $sourceLangcode = 'en';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The language code of the translation language for this test.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $translationLangcode = 'af';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installEntitySchema('entity_test_with_bundle');
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode($this->translationLangcode)->save();
 | 
			
		||||
 | 
			
		||||
    $this->contentTranslationManager = $this->container->get('content_translation.manager');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that content translation fields are created upon module installation.
 | 
			
		||||
   */
 | 
			
		||||
  public function testFieldUpdates(): void {
 | 
			
		||||
    // The module ships a translatable bundle of the 'entity_test_with_bundle'
 | 
			
		||||
    // entity type.
 | 
			
		||||
    $this->installConfig(['content_translation_test']);
 | 
			
		||||
 | 
			
		||||
    $entity = EntityTestWithBundle::create([
 | 
			
		||||
      'type' => 'test',
 | 
			
		||||
      'langcode' => $this->sourceLangcode,
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    // Add a translation with some translation metadata.
 | 
			
		||||
    $translation = $entity->addTranslation($this->translationLangcode);
 | 
			
		||||
    $translation_metadata = $this->contentTranslationManager->getTranslationMetadata($translation);
 | 
			
		||||
    $translation_metadata->setSource($this->sourceLangcode)->setOutdated(TRUE);
 | 
			
		||||
    $translation->save();
 | 
			
		||||
 | 
			
		||||
    // Make sure the translation metadata has been saved correctly.
 | 
			
		||||
    $entity = EntityTestWithBundle::load($entity->id());
 | 
			
		||||
    $translation = $entity->getTranslation($this->translationLangcode);
 | 
			
		||||
    $translation_metadata = $this->contentTranslationManager->getTranslationMetadata($translation);
 | 
			
		||||
    $this->assertSame($this->sourceLangcode, $translation_metadata->getSource());
 | 
			
		||||
    $this->assertTrue($translation_metadata->isOutdated());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,57 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestMulBundle;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the content translation dynamic permissions.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 *
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_translation\ContentTranslationPermissions
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationPermissionsTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['system', 'language', 'content_translation', 'user', 'entity_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->installEntitySchema('entity_test_mul');
 | 
			
		||||
    $this->installEntitySchema('entity_test_mul_with_bundle');
 | 
			
		||||
    EntityTestMulBundle::create([
 | 
			
		||||
      'id' => 'test',
 | 
			
		||||
      'label' => 'Test label',
 | 
			
		||||
      'description' => 'My test description',
 | 
			
		||||
    ])->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that enabling translation via the API triggers schema updates.
 | 
			
		||||
   */
 | 
			
		||||
  public function testPermissions(): void {
 | 
			
		||||
    $this->container->get('content_translation.manager')->setEnabled('entity_test_mul', 'entity_test_mul', TRUE);
 | 
			
		||||
    $this->container->get('content_translation.manager')->setEnabled('entity_test_mul_with_bundle', 'test', TRUE);
 | 
			
		||||
    $permissions = $this->container->get('user.permissions')->getPermissions();
 | 
			
		||||
    $this->assertEquals(['entity_test'], $permissions['translate entity_test_mul']['dependencies']['module']);
 | 
			
		||||
    $this->assertEquals(['entity_test.entity_test_mul_bundle.test'], $permissions['translate test entity_test_mul_with_bundle']['dependencies']['config']);
 | 
			
		||||
 | 
			
		||||
    // Ensure bundle permission granularity works for bundles not based on
 | 
			
		||||
    // configuration.
 | 
			
		||||
    $this->container->get('state')->set('entity_test_mul.permission_granularity', 'bundle');
 | 
			
		||||
    $this->container->get('entity_type.manager')->clearCachedDefinitions();
 | 
			
		||||
    $permissions = $this->container->get('user.permissions')->getPermissions();
 | 
			
		||||
    $this->assertEquals(['entity_test'], $permissions['translate entity_test_mul entity_test_mul']['dependencies']['module']);
 | 
			
		||||
    $this->assertEquals(['entity_test.entity_test_mul_bundle.test'], $permissions['translate test entity_test_mul_with_bundle']['dependencies']['config']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,47 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the content translation settings API.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationSettingsApiTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'user',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->installEntitySchema('entity_test_mul');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that enabling translation via the API triggers schema updates.
 | 
			
		||||
   */
 | 
			
		||||
  public function testSettingsApi(): void {
 | 
			
		||||
    $this->container->get('content_translation.manager')->setEnabled('entity_test_mul', 'entity_test_mul', TRUE);
 | 
			
		||||
    $schema = Database::getConnection()->schema();
 | 
			
		||||
    $result =
 | 
			
		||||
      $schema->fieldExists('entity_test_mul_property_data', 'content_translation_source') &&
 | 
			
		||||
      $schema->fieldExists('entity_test_mul_property_data', 'content_translation_outdated');
 | 
			
		||||
    $this->assertTrue($result, 'Schema updates correctly performed.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,268 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\content_translation\FieldTranslationSynchronizer;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the field synchronization logic.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationSyncUnitTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The synchronizer class to be tested.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\content_translation\FieldTranslationSynchronizer
 | 
			
		||||
   */
 | 
			
		||||
  protected $synchronizer;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The columns to be synchronized.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $synchronized;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * All the field columns.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $columns;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The available language codes.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $langcodes;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The field cardinality.
 | 
			
		||||
   *
 | 
			
		||||
   * @var int
 | 
			
		||||
   */
 | 
			
		||||
  protected $cardinality;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The unchanged field values.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $unchangedFieldValues;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['language', 'content_translation'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->synchronizer = new FieldTranslationSynchronizer($this->container->get('entity_type.manager'), $this->container->get('plugin.manager.field.field_type'));
 | 
			
		||||
    $this->synchronized = ['sync1', 'sync2'];
 | 
			
		||||
    $this->columns = array_merge($this->synchronized, ['var1', 'var2']);
 | 
			
		||||
    $this->langcodes = ['en', 'it', 'fr', 'de', 'es'];
 | 
			
		||||
    $this->cardinality = 4;
 | 
			
		||||
    $this->unchangedFieldValues = [];
 | 
			
		||||
 | 
			
		||||
    // Set up an initial set of values in the correct state, that is with
 | 
			
		||||
    // "synchronized" values being equal.
 | 
			
		||||
    foreach ($this->langcodes as $langcode) {
 | 
			
		||||
      for ($delta = 0; $delta < $this->cardinality; $delta++) {
 | 
			
		||||
        foreach ($this->columns as $column) {
 | 
			
		||||
          $sync = in_array($column, $this->synchronized) && $langcode != $this->langcodes[0];
 | 
			
		||||
          $value = $sync ? $this->unchangedFieldValues[$this->langcodes[0]][$delta][$column] : $langcode . '-' . $delta . '-' . $column;
 | 
			
		||||
          $this->unchangedFieldValues[$langcode][$delta][$column] = $value;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the field synchronization algorithm.
 | 
			
		||||
   */
 | 
			
		||||
  public function testFieldSync(): void {
 | 
			
		||||
    // Add a new item to the source items and check that its added to all the
 | 
			
		||||
    // translations.
 | 
			
		||||
    $sync_langcode = $this->langcodes[2];
 | 
			
		||||
    $unchanged_items = $this->unchangedFieldValues[$sync_langcode];
 | 
			
		||||
    $field_values = $this->unchangedFieldValues;
 | 
			
		||||
    $item = [];
 | 
			
		||||
    foreach ($this->columns as $column) {
 | 
			
		||||
      $item[$column] = $this->randomMachineName();
 | 
			
		||||
    }
 | 
			
		||||
    $field_values[$sync_langcode][] = $item;
 | 
			
		||||
    $this->synchronizer->synchronizeItems($field_values, $unchanged_items, $sync_langcode, $this->langcodes, $this->synchronized);
 | 
			
		||||
    $result = TRUE;
 | 
			
		||||
    foreach ($this->unchangedFieldValues as $langcode => $items) {
 | 
			
		||||
      // Check that the old values are still in place.
 | 
			
		||||
      for ($delta = 0; $delta < $this->cardinality; $delta++) {
 | 
			
		||||
        foreach ($this->columns as $column) {
 | 
			
		||||
          $result = $result && ($this->unchangedFieldValues[$langcode][$delta][$column] == $field_values[$langcode][$delta][$column]);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      // Check that the new item is available in all languages.
 | 
			
		||||
      foreach ($this->columns as $column) {
 | 
			
		||||
        $result = $result && ($field_values[$langcode][$delta][$column] == $field_values[$sync_langcode][$delta][$column]);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    $this->assertTrue($result, 'A new item has been correctly synchronized.');
 | 
			
		||||
 | 
			
		||||
    // Remove an item from the source items and check that its removed from all
 | 
			
		||||
    // the translations.
 | 
			
		||||
    $sync_langcode = $this->langcodes[1];
 | 
			
		||||
    $unchanged_items = $this->unchangedFieldValues[$sync_langcode];
 | 
			
		||||
    $field_values = $this->unchangedFieldValues;
 | 
			
		||||
    $sync_delta = mt_rand(0, count($field_values[$sync_langcode]) - 1);
 | 
			
		||||
    unset($field_values[$sync_langcode][$sync_delta]);
 | 
			
		||||
    // Renumber deltas to start from 0.
 | 
			
		||||
    $field_values[$sync_langcode] = array_values($field_values[$sync_langcode]);
 | 
			
		||||
    $this->synchronizer->synchronizeItems($field_values, $unchanged_items, $sync_langcode, $this->langcodes, $this->synchronized);
 | 
			
		||||
    $result = TRUE;
 | 
			
		||||
    foreach ($this->unchangedFieldValues as $langcode => $items) {
 | 
			
		||||
      $new_delta = 0;
 | 
			
		||||
      // Check that the old values are still in place.
 | 
			
		||||
      for ($delta = 0; $delta < $this->cardinality; $delta++) {
 | 
			
		||||
        // Skip the removed item.
 | 
			
		||||
        if ($delta != $sync_delta) {
 | 
			
		||||
          foreach ($this->columns as $column) {
 | 
			
		||||
            $result = $result && ($this->unchangedFieldValues[$langcode][$delta][$column] == $field_values[$langcode][$new_delta][$column]);
 | 
			
		||||
          }
 | 
			
		||||
          $new_delta++;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    $this->assertTrue($result, 'A removed item has been correctly synchronized.');
 | 
			
		||||
 | 
			
		||||
    // Move the items around in the source items and check that they are moved
 | 
			
		||||
    // in all the translations.
 | 
			
		||||
    $sync_langcode = $this->langcodes[3];
 | 
			
		||||
    $unchanged_items = $this->unchangedFieldValues[$sync_langcode];
 | 
			
		||||
    $field_values = $this->unchangedFieldValues;
 | 
			
		||||
    $field_values[$sync_langcode] = [];
 | 
			
		||||
    // Scramble the items.
 | 
			
		||||
    foreach ($unchanged_items as $delta => $item) {
 | 
			
		||||
      $new_delta = ($delta + 1) % $this->cardinality;
 | 
			
		||||
      $field_values[$sync_langcode][$new_delta] = $item;
 | 
			
		||||
    }
 | 
			
		||||
    // Renumber deltas to start from 0.
 | 
			
		||||
    ksort($field_values[$sync_langcode]);
 | 
			
		||||
    $this->synchronizer->synchronizeItems($field_values, $unchanged_items, $sync_langcode, $this->langcodes, $this->synchronized);
 | 
			
		||||
    $result = TRUE;
 | 
			
		||||
    foreach ($field_values as $langcode => $items) {
 | 
			
		||||
      for ($delta = 0; $delta < $this->cardinality; $delta++) {
 | 
			
		||||
        foreach ($this->columns as $column) {
 | 
			
		||||
          $value = $field_values[$langcode][$delta][$column];
 | 
			
		||||
          if (in_array($column, $this->synchronized)) {
 | 
			
		||||
            // If we are dealing with a synchronize column the current value is
 | 
			
		||||
            // supposed to be the same of the source items.
 | 
			
		||||
            $result = $result && $field_values[$sync_langcode][$delta][$column] == $value;
 | 
			
		||||
          }
 | 
			
		||||
          else {
 | 
			
		||||
            // Otherwise the values should be unchanged.
 | 
			
		||||
            $old_delta = ($delta > 0 ? $delta : $this->cardinality) - 1;
 | 
			
		||||
            $result = $result && $this->unchangedFieldValues[$langcode][$old_delta][$column] == $value;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    $this->assertTrue($result, 'Scrambled items have been correctly synchronized.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that items holding the same values are correctly synchronized.
 | 
			
		||||
   */
 | 
			
		||||
  public function testMultipleSyncedValues(): void {
 | 
			
		||||
    $sync_langcode = $this->langcodes[1];
 | 
			
		||||
    $unchanged_items = $this->unchangedFieldValues[$sync_langcode];
 | 
			
		||||
 | 
			
		||||
    // Determine whether the unchanged values should be altered depending on
 | 
			
		||||
    // their delta.
 | 
			
		||||
    $delta_callbacks = [
 | 
			
		||||
      // Continuous field values: all values are equal.
 | 
			
		||||
      function ($delta) {
 | 
			
		||||
        return TRUE;
 | 
			
		||||
      },
 | 
			
		||||
      // Alternated field values: only the even ones are equal.
 | 
			
		||||
      function ($delta) {
 | 
			
		||||
        return $delta % 2 !== 0;
 | 
			
		||||
      },
 | 
			
		||||
      // Sparse field values: only the "middle" ones are equal.
 | 
			
		||||
      function ($delta) {
 | 
			
		||||
        return $delta === 1 || $delta === 2;
 | 
			
		||||
      },
 | 
			
		||||
      // Sparse field values: only the "extreme" ones are equal.
 | 
			
		||||
      function ($delta) {
 | 
			
		||||
        return $delta === 0 || $delta === 3;
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    foreach ($delta_callbacks as $delta_callback) {
 | 
			
		||||
      $field_values = $this->unchangedFieldValues;
 | 
			
		||||
 | 
			
		||||
      for ($delta = 0; $delta < $this->cardinality; $delta++) {
 | 
			
		||||
        if ($delta_callback($delta)) {
 | 
			
		||||
          foreach ($this->columns as $column) {
 | 
			
		||||
            if (in_array($column, $this->synchronized)) {
 | 
			
		||||
              $field_values[$sync_langcode][$delta][$column] = $field_values[$sync_langcode][0][$column];
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      $changed_items = $field_values[$sync_langcode];
 | 
			
		||||
      $this->synchronizer->synchronizeItems($field_values, $unchanged_items, $sync_langcode, $this->langcodes, $this->synchronized);
 | 
			
		||||
 | 
			
		||||
      foreach ($this->unchangedFieldValues as $langcode => $unchanged_items) {
 | 
			
		||||
        for ($delta = 0; $delta < $this->cardinality; $delta++) {
 | 
			
		||||
          foreach ($this->columns as $column) {
 | 
			
		||||
            // The first item is always unchanged hence it is retained by the
 | 
			
		||||
            // synchronization process. The other ones are retained or synced
 | 
			
		||||
            // depending on the logic implemented by the delta callback and
 | 
			
		||||
            // whether it is a sync column or not.
 | 
			
		||||
            $value = $delta > 0 && $delta_callback($delta) && in_array($column, $this->synchronized) ? $changed_items[0][$column] : $unchanged_items[$delta][$column];
 | 
			
		||||
            $this->assertEquals($field_values[$langcode][$delta][$column], $value, "Item $delta column $column for langcode $langcode synced correctly");
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that one change in a synchronized column triggers a change in all columns.
 | 
			
		||||
   */
 | 
			
		||||
  public function testDifferingSyncedColumns(): void {
 | 
			
		||||
    $sync_langcode = $this->langcodes[2];
 | 
			
		||||
    $unchanged_items = $this->unchangedFieldValues[$sync_langcode];
 | 
			
		||||
    $field_values = $this->unchangedFieldValues;
 | 
			
		||||
 | 
			
		||||
    for ($delta = 0; $delta < $this->cardinality; $delta++) {
 | 
			
		||||
      $index = ($delta % 2) + 1;
 | 
			
		||||
      $field_values[$sync_langcode][$delta]['sync' . $index] .= '-updated';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $changed_items = $field_values[$sync_langcode];
 | 
			
		||||
    $this->synchronizer->synchronizeItems($field_values, $unchanged_items, $sync_langcode, $this->langcodes, $this->synchronized);
 | 
			
		||||
 | 
			
		||||
    foreach ($this->unchangedFieldValues as $langcode => $unchanged_items) {
 | 
			
		||||
      for ($delta = 0; $delta < $this->cardinality; $delta++) {
 | 
			
		||||
        foreach ($this->columns as $column) {
 | 
			
		||||
          // If the column is synchronized, the value should have been synced,
 | 
			
		||||
          // for columns that are not synchronized, the value must not change.
 | 
			
		||||
          $expected_value = in_array($column, $this->synchronized) ? $changed_items[$delta][$column] : $this->unchangedFieldValues[$langcode][$delta][$column];
 | 
			
		||||
          $this->assertEquals($expected_value, $field_values[$langcode][$delta][$column], "Differing Item {$delta} column {$column} for langcode {$langcode} synced correctly");
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,200 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Kernel\Migrate\d6;
 | 
			
		||||
 | 
			
		||||
use Drupal\taxonomy\Entity\Term;
 | 
			
		||||
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
 | 
			
		||||
use Drupal\taxonomy\TermInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test migration of translated taxonomy terms.
 | 
			
		||||
 *
 | 
			
		||||
 * @group migrate_drupal_6
 | 
			
		||||
 */
 | 
			
		||||
class MigrateTaxonomyTermTranslationTest extends MigrateDrupal6TestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'language',
 | 
			
		||||
    'menu_ui',
 | 
			
		||||
    'node',
 | 
			
		||||
    'taxonomy',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The cached taxonomy tree items, keyed by vid and tid.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $treeData = [];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->installEntitySchema('taxonomy_term');
 | 
			
		||||
    $this->installConfig(static::$modules);
 | 
			
		||||
    $this->executeMigrations([
 | 
			
		||||
      'language',
 | 
			
		||||
      'd6_node_type',
 | 
			
		||||
      'd6_field',
 | 
			
		||||
      'd6_taxonomy_vocabulary',
 | 
			
		||||
      'd6_field_instance',
 | 
			
		||||
      'd6_taxonomy_term',
 | 
			
		||||
      'd6_taxonomy_term_translation',
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Validate a migrated term contains the expected values.
 | 
			
		||||
   *
 | 
			
		||||
   * @param int $id
 | 
			
		||||
   *   Entity ID to load and check.
 | 
			
		||||
   * @param string $expected_language
 | 
			
		||||
   *   The language code for this term.
 | 
			
		||||
   * @param string $expected_label
 | 
			
		||||
   *   The label the migrated entity should have.
 | 
			
		||||
   * @param string $expected_vid
 | 
			
		||||
   *   The parent vocabulary the migrated entity should have.
 | 
			
		||||
   * @param string $expected_description
 | 
			
		||||
   *   The description the migrated entity should have.
 | 
			
		||||
   * @param string $expected_format
 | 
			
		||||
   *   The format the migrated entity should have.
 | 
			
		||||
   * @param int $expected_weight
 | 
			
		||||
   *   The weight the migrated entity should have.
 | 
			
		||||
   * @param array $expected_parents
 | 
			
		||||
   *   The parent terms the migrated entity should have.
 | 
			
		||||
   * @param int $expected_field_integer_value
 | 
			
		||||
   *   The value the migrated entity field should have.
 | 
			
		||||
   * @param int $expected_term_reference_tid
 | 
			
		||||
   *   The term reference ID the migrated entity field should have.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertEntity(int $id, string $expected_language, string $expected_label, string $expected_vid, string $expected_description = '', ?string $expected_format = NULL, int $expected_weight = 0, array $expected_parents = [], ?int $expected_field_integer_value = NULL, ?int $expected_term_reference_tid = NULL): void {
 | 
			
		||||
    /** @var \Drupal\taxonomy\TermInterface $entity */
 | 
			
		||||
    $entity = Term::load($id);
 | 
			
		||||
    $this->assertInstanceOf(TermInterface::class, $entity);
 | 
			
		||||
    $this->assertSame($expected_language, $entity->language()->getId());
 | 
			
		||||
    $this->assertSame($expected_label, $entity->label());
 | 
			
		||||
    $this->assertSame($expected_vid, $entity->bundle());
 | 
			
		||||
    $this->assertSame($expected_description, $entity->getDescription());
 | 
			
		||||
    $this->assertSame($expected_format, $entity->getFormat());
 | 
			
		||||
    $this->assertSame($expected_weight, $entity->getWeight());
 | 
			
		||||
    $this->assertHierarchy($expected_vid, $id, $expected_parents);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Assert that a term is present in the tree storage, with the right parents.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $vid
 | 
			
		||||
   *   Vocabulary ID.
 | 
			
		||||
   * @param int $tid
 | 
			
		||||
   *   ID of the term to check.
 | 
			
		||||
   * @param array $parent_ids
 | 
			
		||||
   *   The expected parent term IDs.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertHierarchy(string $vid, int $tid, array $parent_ids): void {
 | 
			
		||||
    if (!isset($this->treeData[$vid])) {
 | 
			
		||||
      $tree = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadTree($vid);
 | 
			
		||||
      $this->treeData[$vid] = [];
 | 
			
		||||
      foreach ($tree as $item) {
 | 
			
		||||
        $this->treeData[$vid][$item->tid] = $item;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $this->assertArrayHasKey($tid, $this->treeData[$vid], "Term $tid exists in taxonomy tree");
 | 
			
		||||
    $term = $this->treeData[$vid][$tid];
 | 
			
		||||
    // PostgreSQL, MySQL and SQLite may not return the parent terms in the same
 | 
			
		||||
    // order so sort before testing.
 | 
			
		||||
    sort($parent_ids);
 | 
			
		||||
    $actual_terms = array_filter($term->parents);
 | 
			
		||||
    sort($actual_terms);
 | 
			
		||||
    $this->assertEquals($parent_ids, $actual_terms, "Term $tid has correct parents in taxonomy tree");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the Drupal 6 i18n taxonomy term to Drupal 8 migration.
 | 
			
		||||
   */
 | 
			
		||||
  public function testTranslatedTaxonomyTerms(): void {
 | 
			
		||||
    $this->assertEntity(
 | 
			
		||||
     1,
 | 
			
		||||
     'zu',
 | 
			
		||||
     'zu - term 1 of vocabulary 1',
 | 
			
		||||
     'vocabulary_1_i_0_',
 | 
			
		||||
     'zu - description of term 1 of vocabulary 1',
 | 
			
		||||
     NULL,
 | 
			
		||||
     0,
 | 
			
		||||
     []
 | 
			
		||||
    );
 | 
			
		||||
    $this->assertEntity(
 | 
			
		||||
     2,
 | 
			
		||||
     'fr',
 | 
			
		||||
     'fr - term 2 of vocabulary 2',
 | 
			
		||||
     'vocabulary_2_i_1_',
 | 
			
		||||
     'fr - description of term 2 of vocabulary 2',
 | 
			
		||||
     NULL,
 | 
			
		||||
     3,
 | 
			
		||||
     []
 | 
			
		||||
    );
 | 
			
		||||
    $this->assertEntity(
 | 
			
		||||
     3,
 | 
			
		||||
     'fr',
 | 
			
		||||
     'fr - term 3 of vocabulary 2',
 | 
			
		||||
     'vocabulary_2_i_1_',
 | 
			
		||||
     'fr - description of term 3 of vocabulary 2',
 | 
			
		||||
     NULL,
 | 
			
		||||
     4,
 | 
			
		||||
     ['2']
 | 
			
		||||
    );
 | 
			
		||||
    $this->assertEntity(
 | 
			
		||||
     4,
 | 
			
		||||
     'en',
 | 
			
		||||
     'term 4 of vocabulary 3',
 | 
			
		||||
     'vocabulary_3_i_2_',
 | 
			
		||||
     'description of term 4 of vocabulary 3',
 | 
			
		||||
     NULL,
 | 
			
		||||
     6,
 | 
			
		||||
     []
 | 
			
		||||
    );
 | 
			
		||||
    $this->assertEntity(
 | 
			
		||||
     5,
 | 
			
		||||
     'en',
 | 
			
		||||
     'term 5 of vocabulary 3',
 | 
			
		||||
     'vocabulary_3_i_2_',
 | 
			
		||||
     'description of term 5 of vocabulary 3',
 | 
			
		||||
     NULL,
 | 
			
		||||
     7,
 | 
			
		||||
     ['4']
 | 
			
		||||
    );
 | 
			
		||||
    $this->assertEntity(
 | 
			
		||||
     6,
 | 
			
		||||
     'en',
 | 
			
		||||
     'term 6 of vocabulary 3',
 | 
			
		||||
     'vocabulary_3_i_2_',
 | 
			
		||||
     'description of term 6 of vocabulary 3',
 | 
			
		||||
     NULL,
 | 
			
		||||
     8,
 | 
			
		||||
     ['4', '5']
 | 
			
		||||
    );
 | 
			
		||||
    $this->assertEntity(
 | 
			
		||||
     7,
 | 
			
		||||
     'fr',
 | 
			
		||||
     'fr - term 2 of vocabulary 1',
 | 
			
		||||
     'vocabulary_1_i_0_',
 | 
			
		||||
     'fr - desc of term 2 vocab 1',
 | 
			
		||||
     NULL,
 | 
			
		||||
     0,
 | 
			
		||||
     []
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,100 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Kernel\Migrate\d7;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Language\LanguageInterface;
 | 
			
		||||
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the migration of entity translation settings.
 | 
			
		||||
 *
 | 
			
		||||
 * @group migrate_drupal_7
 | 
			
		||||
 */
 | 
			
		||||
class MigrateEntityTranslationSettingsTest extends MigrateDrupal7TestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'comment',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'language',
 | 
			
		||||
    'menu_ui',
 | 
			
		||||
    'node',
 | 
			
		||||
    'taxonomy',
 | 
			
		||||
    'text',
 | 
			
		||||
    'user',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installConfig([
 | 
			
		||||
      'comment',
 | 
			
		||||
      'content_translation',
 | 
			
		||||
      'node',
 | 
			
		||||
      'taxonomy',
 | 
			
		||||
      'user',
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    $this->installEntitySchema('comment');
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
    $this->installEntitySchema('taxonomy_term');
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
 | 
			
		||||
    $this->executeMigrations([
 | 
			
		||||
      'language',
 | 
			
		||||
      'd7_comment_type',
 | 
			
		||||
      'd7_node_type',
 | 
			
		||||
      'd7_taxonomy_vocabulary',
 | 
			
		||||
      'd7_entity_translation_settings',
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests entity translation settings migration.
 | 
			
		||||
   */
 | 
			
		||||
  public function testEntityTranslationSettingsMigration(): void {
 | 
			
		||||
    // Tests 'comment_node_test_content_type' entity translation settings.
 | 
			
		||||
    $config = $this->config('language.content_settings.comment.comment_node_test_content_type');
 | 
			
		||||
    $this->assertSame($config->get('target_entity_type_id'), 'comment');
 | 
			
		||||
    $this->assertSame($config->get('target_bundle'), 'comment_node_test_content_type');
 | 
			
		||||
    $this->assertSame($config->get('default_langcode'), 'current_interface');
 | 
			
		||||
    $this->assertFalse((bool) $config->get('language_alterable'));
 | 
			
		||||
    $this->assertTrue((bool) $config->get('third_party_settings.content_translation.enabled'));
 | 
			
		||||
    $this->assertFalse((bool) $config->get('third_party_settings.content_translation.bundle_settings.untranslatable_fields_hide'));
 | 
			
		||||
 | 
			
		||||
    // Tests 'test_content_type' entity translation settings.
 | 
			
		||||
    $config = $this->config('language.content_settings.node.test_content_type');
 | 
			
		||||
    $this->assertSame($config->get('target_entity_type_id'), 'node');
 | 
			
		||||
    $this->assertSame($config->get('target_bundle'), 'test_content_type');
 | 
			
		||||
    $this->assertSame($config->get('default_langcode'), LanguageInterface::LANGCODE_NOT_SPECIFIED);
 | 
			
		||||
    $this->assertTrue((bool) $config->get('language_alterable'));
 | 
			
		||||
    $this->assertTrue((bool) $config->get('third_party_settings.content_translation.enabled'));
 | 
			
		||||
    $this->assertFalse((bool) $config->get('third_party_settings.content_translation.bundle_settings.untranslatable_fields_hide'));
 | 
			
		||||
 | 
			
		||||
    // Tests 'test_vocabulary' entity translation settings.
 | 
			
		||||
    $config = $this->config('language.content_settings.taxonomy_term.test_vocabulary');
 | 
			
		||||
    $this->assertSame($config->get('target_entity_type_id'), 'taxonomy_term');
 | 
			
		||||
    $this->assertSame($config->get('target_bundle'), 'test_vocabulary');
 | 
			
		||||
    $this->assertSame($config->get('default_langcode'), LanguageInterface::LANGCODE_SITE_DEFAULT);
 | 
			
		||||
    $this->assertFalse((bool) $config->get('language_alterable'));
 | 
			
		||||
    $this->assertTrue((bool) $config->get('third_party_settings.content_translation.enabled'));
 | 
			
		||||
    $this->assertFalse((bool) $config->get('third_party_settings.content_translation.bundle_settings.untranslatable_fields_hide'));
 | 
			
		||||
 | 
			
		||||
    // Tests 'user' entity translation settings.
 | 
			
		||||
    $config = $this->config('language.content_settings.user.user');
 | 
			
		||||
    $this->assertSame($config->get('target_entity_type_id'), 'user');
 | 
			
		||||
    $this->assertSame($config->get('target_bundle'), 'user');
 | 
			
		||||
    $this->assertSame($config->get('default_langcode'), LanguageInterface::LANGCODE_SITE_DEFAULT);
 | 
			
		||||
    $this->assertFalse((bool) $config->get('language_alterable'));
 | 
			
		||||
    $this->assertTrue((bool) $config->get('third_party_settings.content_translation.enabled'));
 | 
			
		||||
    $this->assertFalse((bool) $config->get('third_party_settings.content_translation.bundle_settings.untranslatable_fields_hide'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,256 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Kernel\Plugin\migrate\source\d7;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests entity translation settings source plugin.
 | 
			
		||||
 *
 | 
			
		||||
 * @covers \Drupal\content_translation\Plugin\migrate\source\d7\EntityTranslationSettings
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class EntityTranslationSettingsTest extends MigrateSqlSourceTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'language',
 | 
			
		||||
    'migrate_drupal',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public static function providerSource() {
 | 
			
		||||
    $tests = [];
 | 
			
		||||
 | 
			
		||||
    // Source data when there's no entity type that uses entity translation.
 | 
			
		||||
    $tests[0]['source_data']['variable'] = [
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'entity_translation_entity_types',
 | 
			
		||||
        'value' => 'a:4:{s:7:"comment";i:0;s:4:"node";i:0;s:13:"taxonomy_term";i:0;s:4:"user";i:0;}',
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Source data when there's no bundle settings variables.
 | 
			
		||||
    $tests[1]['source_data']['variable'] = [
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'entity_translation_entity_types',
 | 
			
		||||
        'value' => 'a:4:{s:7:"comment";s:7:"comment";s:4:"node";s:4:"node";s:13:"taxonomy_term";s:13:"taxonomy_term";s:4:"user";s:4:"user";}',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'entity_translation_taxonomy',
 | 
			
		||||
        'value' => 'a:3:{s:6:"forums";b:1;s:4:"tags";b:1;s:4:"test";b:0;}',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'language_content_type_article',
 | 
			
		||||
        'value' => 's:1:"2";',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'language_content_type_forum',
 | 
			
		||||
        'value' => 's:1:"4";',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'language_content_type_page',
 | 
			
		||||
        'value' => 's:1:"4";',
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Source data when there's bundle settings variables.
 | 
			
		||||
    $tests[2]['source_data']['variable'] = [
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'entity_translation_entity_types',
 | 
			
		||||
        'value' => 'a:4:{s:7:"comment";s:7:"comment";s:4:"node";s:4:"node";s:13:"taxonomy_term";s:13:"taxonomy_term";s:4:"user";s:4:"user";}',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'entity_translation_settings_comment__comment_node_forum',
 | 
			
		||||
        'value' => 'a:5:{s:16:"default_language";s:12:"xx-et-author";s:22:"hide_language_selector";i:1;s:21:"exclude_language_none";i:0;s:13:"lock_language";i:0;s:27:"shared_fields_original_only";i:0;}',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'entity_translation_settings_comment__comment_node_page',
 | 
			
		||||
        'value' => 'a:5:{s:16:"default_language";s:12:"xx-et-author";s:22:"hide_language_selector";i:0;s:21:"exclude_language_none";i:0;s:13:"lock_language";i:0;s:27:"shared_fields_original_only";i:1;}',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'entity_translation_settings_node__forum',
 | 
			
		||||
        'value' => 'a:5:{s:16:"default_language";s:12:"xx-et-author";s:22:"hide_language_selector";i:0;s:21:"exclude_language_none";i:0;s:13:"lock_language";i:0;s:27:"shared_fields_original_only";i:0;}',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'entity_translation_settings_node__page',
 | 
			
		||||
        'value' => 'a:5:{s:16:"default_language";s:13:"xx-et-default";s:22:"hide_language_selector";i:1;s:21:"exclude_language_none";i:0;s:13:"lock_language";i:0;s:27:"shared_fields_original_only";i:1;}',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'entity_translation_settings_taxonomy_term__forums',
 | 
			
		||||
        'value' => 'a:5:{s:16:"default_language";s:13:"xx-et-current";s:22:"hide_language_selector";i:0;s:21:"exclude_language_none";i:0;s:13:"lock_language";i:0;s:27:"shared_fields_original_only";i:1;}',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'entity_translation_settings_taxonomy_term__tags',
 | 
			
		||||
        'value' => 'a:5:{s:16:"default_language";s:13:"xx-et-current";s:22:"hide_language_selector";i:1;s:21:"exclude_language_none";i:0;s:13:"lock_language";i:0;s:27:"shared_fields_original_only";i:0;}',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'entity_translation_settings_user__user',
 | 
			
		||||
        'value' => 'a:5:{s:16:"default_language";s:12:"xx-et-author";s:22:"hide_language_selector";i:1;s:21:"exclude_language_none";i:0;s:13:"lock_language";i:0;s:27:"shared_fields_original_only";i:1;}',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'entity_translation_taxonomy',
 | 
			
		||||
        'value' => 'a:3:{s:6:"forums";b:1;s:4:"tags";b:1;s:4:"test";b:0;}',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'language_content_type_article',
 | 
			
		||||
        'value' => 's:1:"2";',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'language_content_type_forum',
 | 
			
		||||
        'value' => 's:1:"4";',
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'language_content_type_page',
 | 
			
		||||
        'value' => 's:1:"4";',
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Source data when taxonomy terms are translatable but the
 | 
			
		||||
    // 'entity_translation_taxonomy' variable is not set.
 | 
			
		||||
    $tests[3]['source_data']['variable'] = [
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'entity_translation_entity_types',
 | 
			
		||||
        'value' => 'a:4:{s:7:"comment";i:0;s:4:"node";i:0;s:13:"taxonomy_term";i:1;s:4:"user";i:0;}',
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Expected data when there's no entity type that uses entity translation.
 | 
			
		||||
    $tests[0]['expected_data'] = [];
 | 
			
		||||
 | 
			
		||||
    // Expected data when there's no bundle settings variables.
 | 
			
		||||
    $tests[1]['expected_data'] = [
 | 
			
		||||
      [
 | 
			
		||||
        'id' => 'node.forum',
 | 
			
		||||
        'target_entity_type_id' => 'node',
 | 
			
		||||
        'target_bundle' => 'forum',
 | 
			
		||||
        'default_langcode' => 'und',
 | 
			
		||||
        'language_alterable' => TRUE,
 | 
			
		||||
        'untranslatable_fields_hide' => FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'id' => 'node.page',
 | 
			
		||||
        'target_entity_type_id' => 'node',
 | 
			
		||||
        'target_bundle' => 'page',
 | 
			
		||||
        'default_langcode' => 'und',
 | 
			
		||||
        'language_alterable' => TRUE,
 | 
			
		||||
        'untranslatable_fields_hide' => FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'id' => 'comment.comment_forum',
 | 
			
		||||
        'target_entity_type_id' => 'comment',
 | 
			
		||||
        'target_bundle' => 'comment_forum',
 | 
			
		||||
        'default_langcode' => 'xx-et-current',
 | 
			
		||||
        'language_alterable' => FALSE,
 | 
			
		||||
        'untranslatable_fields_hide' => FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'id' => 'comment.comment_node_page',
 | 
			
		||||
        'target_entity_type_id' => 'comment',
 | 
			
		||||
        'target_bundle' => 'comment_node_page',
 | 
			
		||||
        'default_langcode' => 'xx-et-current',
 | 
			
		||||
        'language_alterable' => FALSE,
 | 
			
		||||
        'untranslatable_fields_hide' => FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'id' => 'taxonomy_term.forums',
 | 
			
		||||
        'target_entity_type_id' => 'taxonomy_term',
 | 
			
		||||
        'target_bundle' => 'forums',
 | 
			
		||||
        'default_langcode' => 'xx-et-default',
 | 
			
		||||
        'language_alterable' => FALSE,
 | 
			
		||||
        'untranslatable_fields_hide' => FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'id' => 'taxonomy_term.tags',
 | 
			
		||||
        'target_entity_type_id' => 'taxonomy_term',
 | 
			
		||||
        'target_bundle' => 'tags',
 | 
			
		||||
        'default_langcode' => 'xx-et-default',
 | 
			
		||||
        'language_alterable' => FALSE,
 | 
			
		||||
        'untranslatable_fields_hide' => FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'id' => 'user.user',
 | 
			
		||||
        'target_entity_type_id' => 'user',
 | 
			
		||||
        'target_bundle' => 'user',
 | 
			
		||||
        'default_langcode' => 'xx-et-default',
 | 
			
		||||
        'language_alterable' => FALSE,
 | 
			
		||||
        'untranslatable_fields_hide' => FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Expected data when there's bundle settings variables.
 | 
			
		||||
    $tests[2]['expected_data'] = [
 | 
			
		||||
      [
 | 
			
		||||
        'id' => 'node.forum',
 | 
			
		||||
        'target_entity_type_id' => 'node',
 | 
			
		||||
        'target_bundle' => 'forum',
 | 
			
		||||
        'default_langcode' => 'xx-et-author',
 | 
			
		||||
        'language_alterable' => TRUE,
 | 
			
		||||
        'untranslatable_fields_hide' => FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'id' => 'node.page',
 | 
			
		||||
        'target_entity_type_id' => 'node',
 | 
			
		||||
        'target_bundle' => 'page',
 | 
			
		||||
        'default_langcode' => 'xx-et-default',
 | 
			
		||||
        'language_alterable' => FALSE,
 | 
			
		||||
        'untranslatable_fields_hide' => TRUE,
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'id' => 'comment.comment_forum',
 | 
			
		||||
        'target_entity_type_id' => 'comment',
 | 
			
		||||
        'target_bundle' => 'comment_forum',
 | 
			
		||||
        'default_langcode' => 'xx-et-author',
 | 
			
		||||
        'language_alterable' => FALSE,
 | 
			
		||||
        'untranslatable_fields_hide' => FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'id' => 'comment.comment_node_page',
 | 
			
		||||
        'target_entity_type_id' => 'comment',
 | 
			
		||||
        'target_bundle' => 'comment_node_page',
 | 
			
		||||
        'default_langcode' => 'xx-et-author',
 | 
			
		||||
        'language_alterable' => TRUE,
 | 
			
		||||
        'untranslatable_fields_hide' => TRUE,
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'id' => 'taxonomy_term.forums',
 | 
			
		||||
        'target_entity_type_id' => 'taxonomy_term',
 | 
			
		||||
        'target_bundle' => 'forums',
 | 
			
		||||
        'default_langcode' => 'xx-et-current',
 | 
			
		||||
        'language_alterable' => TRUE,
 | 
			
		||||
        'untranslatable_fields_hide' => TRUE,
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'id' => 'taxonomy_term.tags',
 | 
			
		||||
        'target_entity_type_id' => 'taxonomy_term',
 | 
			
		||||
        'target_bundle' => 'tags',
 | 
			
		||||
        'default_langcode' => 'xx-et-current',
 | 
			
		||||
        'language_alterable' => FALSE,
 | 
			
		||||
        'untranslatable_fields_hide' => FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'id' => 'user.user',
 | 
			
		||||
        'target_entity_type_id' => 'user',
 | 
			
		||||
        'target_bundle' => 'user',
 | 
			
		||||
        'default_langcode' => 'xx-et-author',
 | 
			
		||||
        'language_alterable' => FALSE,
 | 
			
		||||
        'untranslatable_fields_hide' => TRUE,
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Expected data when taxonomy terms are translatable but the
 | 
			
		||||
    // 'entity_translation_taxonomy' variable is not set.
 | 
			
		||||
    $tests[3]['expected_data'] = [];
 | 
			
		||||
 | 
			
		||||
    return $tests;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,36 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Traits;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Language\LanguageInterface;
 | 
			
		||||
use Drupal\Tests\language\Traits\LanguageTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides an API to programmatically manage content translation in tests.
 | 
			
		||||
 */
 | 
			
		||||
trait ContentTranslationTestTrait {
 | 
			
		||||
 | 
			
		||||
  use LanguageTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Enables content translation for the given entity type bundle.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $entity_type_id
 | 
			
		||||
   *   The ID of the entity type.
 | 
			
		||||
   * @param string $bundle
 | 
			
		||||
   *   The bundle name.
 | 
			
		||||
   * @param string|null $default_langcode
 | 
			
		||||
   *   The language code to use as the default language.
 | 
			
		||||
   */
 | 
			
		||||
  public function enableContentTranslation(string $entity_type_id, string $bundle, ?string $default_langcode = LanguageInterface::LANGCODE_SITE_DEFAULT): void {
 | 
			
		||||
    static::enableBundleTranslation($entity_type_id, $bundle, $default_langcode);
 | 
			
		||||
    $content_translation_manager = $this->container->get('content_translation.manager');
 | 
			
		||||
    $content_translation_manager->setEnabled($entity_type_id, $bundle, TRUE);
 | 
			
		||||
    $content_translation_manager->setBundleTranslationSettings($entity_type_id, $bundle, [
 | 
			
		||||
      'untranslatable_fields_hide' => FALSE,
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,131 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Unit\Access;
 | 
			
		||||
 | 
			
		||||
use Drupal\content_translation\Access\ContentTranslationManageAccessCheck;
 | 
			
		||||
use Drupal\Core\Access\AccessResult;
 | 
			
		||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
 | 
			
		||||
use Drupal\Core\Cache\Cache;
 | 
			
		||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
 | 
			
		||||
use Drupal\Core\Language\Language;
 | 
			
		||||
use Drupal\Tests\Core\Entity\ContentEntityBaseMockableClass;
 | 
			
		||||
use Drupal\Tests\UnitTestCase;
 | 
			
		||||
use Symfony\Component\Routing\Route;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests for content translation manage check.
 | 
			
		||||
 *
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_translation\Access\ContentTranslationManageAccessCheck
 | 
			
		||||
 * @group Access
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationManageAccessCheckTest extends UnitTestCase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The cache contexts manager.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Cache\Context\CacheContextsManager|\PHPUnit\Framework\MockObject\MockObject
 | 
			
		||||
   */
 | 
			
		||||
  protected $cacheContextsManager;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->cacheContextsManager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
 | 
			
		||||
      ->disableOriginalConstructor()
 | 
			
		||||
      ->getMock();
 | 
			
		||||
    $this->cacheContextsManager->method('assertValidTokens')->willReturn(TRUE);
 | 
			
		||||
 | 
			
		||||
    $container = new ContainerBuilder();
 | 
			
		||||
    $container->set('cache_contexts_manager', $this->cacheContextsManager);
 | 
			
		||||
    \Drupal::setContainer($container);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the create access method.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::access
 | 
			
		||||
   */
 | 
			
		||||
  public function testCreateAccess(): void {
 | 
			
		||||
    // Set the mock translation handler.
 | 
			
		||||
    $translation_handler = $this->createMock('\Drupal\content_translation\ContentTranslationHandlerInterface');
 | 
			
		||||
    $translation_handler->expects($this->once())
 | 
			
		||||
      ->method('getTranslationAccess')
 | 
			
		||||
      ->willReturn(AccessResult::allowed());
 | 
			
		||||
 | 
			
		||||
    $entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
 | 
			
		||||
    $entity_type_manager->expects($this->once())
 | 
			
		||||
      ->method('getHandler')
 | 
			
		||||
      ->withAnyParameters()
 | 
			
		||||
      ->willReturn($translation_handler);
 | 
			
		||||
 | 
			
		||||
    // Set our source and target languages.
 | 
			
		||||
    $source = 'en';
 | 
			
		||||
    $target = 'it';
 | 
			
		||||
 | 
			
		||||
    // Set the mock language manager.
 | 
			
		||||
    $language_manager = $this->createMock('Drupal\Core\Language\LanguageManagerInterface');
 | 
			
		||||
    $language_manager->expects($this->once())
 | 
			
		||||
      ->method('getLanguages')
 | 
			
		||||
      ->willReturn([$source => [], $target => []]);
 | 
			
		||||
    $language_manager->expects($this->any())
 | 
			
		||||
      ->method('getLanguage')
 | 
			
		||||
      ->willReturnMap([
 | 
			
		||||
        [$source, new Language(['id' => $source])],
 | 
			
		||||
        [$target, new Language(['id' => $target])],
 | 
			
		||||
      ]);
 | 
			
		||||
 | 
			
		||||
    // Set the mock entity. We need to use ContentEntityBase for mocking due to
 | 
			
		||||
    // issues with phpunit and multiple interfaces.
 | 
			
		||||
    $entity = $this->getMockBuilder(ContentEntityBaseMockableClass::class)
 | 
			
		||||
      ->disableOriginalConstructor()
 | 
			
		||||
      ->getMock();
 | 
			
		||||
    $entity->expects($this->once())
 | 
			
		||||
      ->method('getEntityTypeId');
 | 
			
		||||
    $entity->expects($this->once())
 | 
			
		||||
      ->method('getTranslationLanguages')
 | 
			
		||||
      ->with()
 | 
			
		||||
      ->willReturn([]);
 | 
			
		||||
    $entity->expects($this->once())
 | 
			
		||||
      ->method('getCacheContexts')
 | 
			
		||||
      ->willReturn([]);
 | 
			
		||||
    $entity->expects($this->once())
 | 
			
		||||
      ->method('getCacheMaxAge')
 | 
			
		||||
      ->willReturn(Cache::PERMANENT);
 | 
			
		||||
    $entity->expects($this->once())
 | 
			
		||||
      ->method('getCacheTags')
 | 
			
		||||
      ->willReturn(['node:1337']);
 | 
			
		||||
    $entity->expects($this->once())
 | 
			
		||||
      ->method('getCacheContexts')
 | 
			
		||||
      ->willReturn([]);
 | 
			
		||||
 | 
			
		||||
    // Set the route requirements.
 | 
			
		||||
    $route = new Route('test_route');
 | 
			
		||||
    $route->setRequirement('_access_content_translation_manage', 'create');
 | 
			
		||||
 | 
			
		||||
    // Set up the route match.
 | 
			
		||||
    $route_match = $this->createMock('Drupal\Core\Routing\RouteMatchInterface');
 | 
			
		||||
    $route_match->expects($this->once())
 | 
			
		||||
      ->method('getParameter')
 | 
			
		||||
      ->with('node')
 | 
			
		||||
      ->willReturn($entity);
 | 
			
		||||
 | 
			
		||||
    // Set the mock account.
 | 
			
		||||
    $account = $this->createMock('Drupal\Core\Session\AccountInterface');
 | 
			
		||||
 | 
			
		||||
    // The access check under test.
 | 
			
		||||
    $check = new ContentTranslationManageAccessCheck($entity_type_manager, $language_manager);
 | 
			
		||||
 | 
			
		||||
    // The request params.
 | 
			
		||||
    $language = 'en';
 | 
			
		||||
    $entity_type_id = 'node';
 | 
			
		||||
 | 
			
		||||
    $this->assertTrue($check->access($route, $route_match, $account, $source, $target, $language, $entity_type_id)->isAllowed(), "The access check matches");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,87 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_translation\Unit\Menu;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\Core\Menu\LocalTaskIntegrationTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests content translation local tasks.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_translation
 | 
			
		||||
 */
 | 
			
		||||
class ContentTranslationLocalTasksTest extends LocalTaskIntegrationTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    $this->directoryList = [
 | 
			
		||||
      'content_translation' => 'core/modules/content_translation',
 | 
			
		||||
      'node' => 'core/modules/node',
 | 
			
		||||
    ];
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $entity_type = $this->createMock('Drupal\Core\Entity\EntityTypeInterface');
 | 
			
		||||
    $entity_type->expects($this->any())
 | 
			
		||||
      ->method('getLinkTemplate')
 | 
			
		||||
      ->willReturnMap([
 | 
			
		||||
        ['canonical', 'entity.node.canonical'],
 | 
			
		||||
        [
 | 
			
		||||
          'drupal:content-translation-overview',
 | 
			
		||||
          'entity.node.content_translation_overview',
 | 
			
		||||
        ],
 | 
			
		||||
      ]);
 | 
			
		||||
    $content_translation_manager = $this->createMock('Drupal\content_translation\ContentTranslationManagerInterface');
 | 
			
		||||
    $content_translation_manager->expects($this->any())
 | 
			
		||||
      ->method('getSupportedEntityTypes')
 | 
			
		||||
      ->willReturn([
 | 
			
		||||
        'node' => $entity_type,
 | 
			
		||||
      ]);
 | 
			
		||||
    \Drupal::getContainer()->set('content_translation.manager', $content_translation_manager);
 | 
			
		||||
    \Drupal::getContainer()->set('string_translation', $this->getStringTranslationStub());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the block admin display local tasks.
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider providerTestBlockAdminDisplay
 | 
			
		||||
   */
 | 
			
		||||
  public function testBlockAdminDisplay($route, $expected): void {
 | 
			
		||||
    $this->assertLocalTasks($route, $expected);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Provides a list of routes to test.
 | 
			
		||||
   */
 | 
			
		||||
  public static function providerTestBlockAdminDisplay() {
 | 
			
		||||
    return [
 | 
			
		||||
      [
 | 
			
		||||
        'entity.node.canonical',
 | 
			
		||||
        [
 | 
			
		||||
          [
 | 
			
		||||
            'content_translation.local_tasks:entity.node.content_translation_overview',
 | 
			
		||||
            'entity.node.canonical',
 | 
			
		||||
            'entity.node.edit_form',
 | 
			
		||||
            'entity.node.delete_form',
 | 
			
		||||
            'entity.node.version_history',
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        'entity.node.content_translation_overview',
 | 
			
		||||
        [
 | 
			
		||||
          [
 | 
			
		||||
            'content_translation.local_tasks:entity.node.content_translation_overview',
 | 
			
		||||
            'entity.node.canonical',
 | 
			
		||||
            'entity.node.edit_form',
 | 
			
		||||
            'entity.node.delete_form',
 | 
			
		||||
            'entity.node.version_history',
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user