Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,6 @@
langcode: en
status: true
dependencies: { }
id: test
label: null
description: null

View File

@ -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

View File

@ -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

View File

@ -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 {
}

View File

@ -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 {
}

View File

@ -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'));
}
}

View File

@ -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

View File

@ -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

View File

@ -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']);
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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]');
}
}

View File

@ -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');
}
}

View File

@ -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);
}
}

View File

@ -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]');
}
}

View File

@ -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.');
}
}

View File

@ -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'));
}
}

View File

@ -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();
}
}

View File

@ -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.');
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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]');
}
}

View File

@ -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');
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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)');
}
}

View File

@ -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('&lt;span class=&quot;translation-entity-all-languages&quot;&gt;(all languages)&lt;/span&gt;');
$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.');
}
}
}

View File

@ -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.');
}
}

View File

@ -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]");
}
}

View File

@ -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 {}

View File

@ -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');
}
}

View File

@ -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.');
}
}

View File

@ -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();
}
}

View File

@ -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'));
}
}

View File

@ -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.');
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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']);
}
}

View File

@ -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.');
}
}

View File

@ -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");
}
}
}
}
}

View File

@ -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,
[]
);
}
}

View File

@ -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'));
}
}

View File

@ -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;
}
}

View File

@ -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,
]);
}
}

View File

@ -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");
}
}

View File

@ -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',
],
],
],
];
}
}