diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php index 312c7d039e31d84ad6345a0439236987f6555b5a..9a52464d7daec351170c42595bdc2242c75645e4 100644 --- a/core/lib/Drupal/Core/Entity/EntityManager.php +++ b/core/lib/Drupal/Core/Entity/EntityManager.php @@ -489,13 +489,23 @@ public function getFieldDefinitions($entity_type, $bundle = NULL) { $hooks = array('entity_field_info', $entity_type . '_field_info'); $this->moduleHandler->alter($hooks, $this->entityFieldInfo[$entity_type], $entity_type); - // Enforce fields to be multiple by default. - foreach ($this->entityFieldInfo[$entity_type]['definitions'] as &$definition) { - $definition['list'] = TRUE; - } - foreach ($this->entityFieldInfo[$entity_type]['optional'] as &$definition) { - $definition['list'] = TRUE; + // Enforce fields to be multiple and untranslatable by default. + $entity_info = $this->getDefinition($entity_type); + $keys = array_intersect_key(array_filter($entity_info['entity_keys']), array_flip(array('id', 'revision', 'uuid', 'bundle'))); + $untranslatable_fields = array_flip(array('langcode') + $keys); + foreach (array('definitions', 'optional') as $key) { + foreach ($this->entityFieldInfo[$entity_type][$key] as $name => &$definition) { + $definition['list'] = TRUE; + // Ensure ids and langcode fields are never made translatable. + if (isset($untranslatable_fields[$name]) && !empty($definition['translatable'])) { + throw new \LogicException(format_string('The @field field cannot be translatable.', array('@field' => $definition['label']))); + } + if (!isset($definition['translatable'])) { + $definition['translatable'] = FALSE; + } + } } + $this->cache->set($cid, $this->entityFieldInfo[$entity_type], CacheBackendInterface::CACHE_PERMANENT, array('entity_info' => TRUE, 'entity_field_info' => TRUE)); } } diff --git a/core/modules/content_translation/content_translation.admin.inc b/core/modules/content_translation/content_translation.admin.inc index d896918f2fda3c088f2a8795ef08f31efd9bb2ff..b2a83b14edc53dd0b7dc60b55c067aa13021ca55 100644 --- a/core/modules/content_translation/content_translation.admin.inc +++ b/core/modules/content_translation/content_translation.admin.inc @@ -8,6 +8,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\Field\FieldDefinitionInterface; use Drupal\Core\Language\Language; +use Drupal\field\Field as FieldService; /** * Returns a form element to configure field synchronization. @@ -73,6 +74,7 @@ function _content_translation_form_language_content_settings_form_alter(array &$ $form['#attached']['js'][] = array('data' => drupal_get_path('module', 'content_translation') . '/content_translation.admin.js', 'type' => 'file'); $dependent_options_settings = array(); + $entity_manager = Drupal::entityManager(); foreach ($form['#labels'] as $entity_type => $label) { $entity_info = entity_get_info($entity_type); foreach (entity_get_bundles($entity_type) as $bundle => $bundle_info) { @@ -84,31 +86,51 @@ function _content_translation_form_language_content_settings_form_alter(array &$ // Only show the checkbox to enable translation if the bundles in the // entity might have fields and if there are fields to translate. if (!empty($entity_info['fieldable'])) { - $instances = field_info_instances($entity_type, $bundle); - if ($instances) { + $fields = $entity_manager->getFieldDefinitions($entity_type, $bundle); + if ($fields) { $form['settings'][$entity_type][$bundle]['translatable'] = array( '#type' => 'checkbox', '#title' => $bundle, '#default_value' => content_translation_enabled($entity_type, $bundle), ); - // @todo Exploit field definitions once all core entities and field - // types are migrated to the Entity Field API. - foreach ($instances as $field_name => $instance) { - $form['settings'][$entity_type][$bundle]['fields'][$field_name] = array( - '#label' => $instance->getFieldLabel(), - '#type' => 'checkbox', - '#title' => $instance->getFieldLabel(), - '#default_value' => $instance->isFieldTranslatable(), - ); - $column_element = content_translation_field_sync_widget($instance); - if ($column_element) { - $form['settings'][$entity_type][$bundle]['columns'][$field_name] = $column_element; - - if (isset($column_element['#options']['file'])) { - $dependent_options_settings["settings[{$entity_type}][{$bundle}][columns][{$field_name}]"] = array('file'); + $field_settings = content_translation_get_config($entity_type, $bundle, 'fields'); + foreach ($fields as $field_name => $definition) { + $translatable = !empty($field_settings[$field_name]); + + // We special case Field API fields as they always natively support + // translation. + // @todo Remove this special casing as soon as configurable and + // base field definitions are "unified". + if (!empty($definition['configurable']) && ($field = FieldService::fieldInfo()->getField($entity_type, $field_name))) { + $instance = FieldService::fieldInfo()->getInstance($entity_type, $bundle, $field_name); + $form['settings'][$entity_type][$bundle]['fields'][$field_name] = array( + '#label' => $instance->getFieldLabel(), + '#type' => 'checkbox', + '#title' => $instance->getFieldLabel(), + '#default_value' => $translatable, + ); + $column_element = content_translation_field_sync_widget($instance); + if ($column_element) { + $form['settings'][$entity_type][$bundle]['columns'][$field_name] = $column_element; + + // @todo This should not concern only files. + if (isset($column_element['#options']['file'])) { + $dependent_options_settings["settings[{$entity_type}][{$bundle}][columns][{$field_name}]"] = array('file'); + } } } + // Instead we need to rely on field definitions to determine whether + // fields support translation. Whether they are actually enabled is + // determined through our settings. As a consequence only fields + // that support translation can be enabled or disabled. + elseif (isset($field_settings[$field_name]) || !empty($definition['translatable'])) { + $form['settings'][$entity_type][$bundle]['fields'][$field_name] = array( + '#label' => $definition['label'], + '#type' => 'checkbox', + '#default_value' => $translatable, + ); + } } } } @@ -326,11 +348,11 @@ function _content_translation_update_field_translatability($settings) { foreach ($bundle_settings['fields'] as $field_name => $translatable) { // If a field is enabled for translation for at least one instance we // need to mark it as translatable. - $fields[$entity_type][$field_name] = $translatable || !empty($fields[$entity_type][$field_name]); + if (FieldService::fieldInfo()->getField($entity_type, $field_name)) { + $fields[$entity_type][$field_name] = $translatable || !empty($fields[$entity_type][$field_name]); + } } } - // @todo Store non-configurable field settings to be able to alter their - // definition afterwards. } } diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index 20d72edaabc816e20cf467964b0437c253af2dc9..fe80ac69a13a7d05e4e50c5e17bea6efb16f1dd6 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -126,6 +126,33 @@ function content_translation_entity_bundle_info_alter(&$bundles) { } } +/** + * Implements hook_entity_field_info_alter(). + */ +function content_translation_entity_field_info_alter(&$info, $entity_type) { + $translation_settings = config('content_translation.settings')->get($entity_type); + + if ($translation_settings) { + // Currently field translatability is defined per-field but we may want to + // make it per-instance instead, so leaving the possibility open for further + // easier refactoring. + $fields = array(); + foreach ($translation_settings as $bundle => $settings) { + $fields += !empty($settings['content_translation']['fields']) ? $settings['content_translation']['fields'] : array(); + } + + $keys = array('definitions', 'optional'); + foreach ($fields as $name => $translatable) { + foreach ($keys as $key) { + if (isset($info[$key][$name])) { + $info[$key][$name]['translatable'] = (bool) $translatable; + break; + } + } + } + } +} + /** * Implements hook_menu(). */ @@ -987,6 +1014,11 @@ function content_translation_save_settings($settings) { // Store whether a bundle has translation enabled or not. content_translation_set_config($entity_type, $bundle, 'enabled', $bundle_settings['translatable']); + // Store whether fields are translatable or not. + if (!empty($bundle_settings['fields'])) { + content_translation_set_config($entity_type, $bundle, 'fields', $bundle_settings['fields']); + } + // Store whether fields have translation enabled or not. if (!empty($bundle_settings['columns'])) { foreach ($bundle_settings['columns'] as $field_name => $column_settings) { diff --git a/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationSettingsTest.php b/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationSettingsTest.php index 7e4664f96460828d2fb131138adcc5dd9f851859..127f7eaad200c71b35b506cd98983e7637a782e6 100644 --- a/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationSettingsTest.php +++ b/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationSettingsTest.php @@ -20,7 +20,7 @@ class ContentTranslationSettingsTest extends WebTestBase { * * @var array */ - public static $modules = array('language', 'content_translation', 'comment', 'field_ui'); + public static $modules = array('language', 'content_translation', 'node', 'comment', 'field_ui'); public static function getInfo() { return array( @@ -114,6 +114,17 @@ function testSettingsUI() { $this->drupalPostForm('admin/structure/types/manage/article', $edit, t('Save content type')); $this->drupalGet('admin/structure/types/manage/article'); $this->assertFieldChecked('edit-language-configuration-content-translation'); + + // Test that the title field of nodes is available in the settings form. + $edit = array( + 'entity_types[node]' => TRUE, + 'settings[node][article][settings][language][langcode]' => 'current_interface', + 'settings[node][article][settings][language][language_show]' => TRUE, + 'settings[node][article][translatable]' => TRUE, + 'settings[node][article][fields][title]' => TRUE + ); + $this->assertSettings('node', NULL, TRUE, $edit); + } /** diff --git a/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationSyncImageTest.php b/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationSyncImageTest.php index 9789dc5f2b7d378d656412c47e6b34e9d9b8588b..f97483798fb188fd1be77f8654b4f7c0772f33ec 100644 --- a/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationSyncImageTest.php +++ b/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationSyncImageTest.php @@ -108,10 +108,13 @@ function testImageFieldSync() { $this->assertNoFieldChecked('edit-settings-entity-test-mul-entity-test-mul-columns-field-test-et-ui-image-alt'); $this->assertNoFieldChecked('edit-settings-entity-test-mul-entity-test-mul-columns-field-test-et-ui-image-title'); $edit = array( + '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->drupalPostForm('admin/config/regional/content-language', $edit, t('Save')); + $errors = $this->xpath('//div[contains(@class, "messages--error")]'); + $this->assertFalse($errors, 'Settings correctly stored.'); $this->assertFieldChecked('edit-settings-entity-test-mul-entity-test-mul-columns-field-test-et-ui-image-alt'); $this->assertFieldChecked('edit-settings-entity-test-mul-entity-test-mul-columns-field-test-et-ui-image-title'); $this->drupalLogin($this->translator); diff --git a/core/modules/node/lib/Drupal/node/Entity/Node.php b/core/modules/node/lib/Drupal/node/Entity/Node.php index 92f4f5b638395a56ee8bb8ba8489d65bc5b8d315..de94ae8b3b4cf4b8dcc1ee6aba628c8b7c80bec4 100644 --- a/core/modules/node/lib/Drupal/node/Entity/Node.php +++ b/core/modules/node/lib/Drupal/node/Entity/Node.php @@ -359,6 +359,7 @@ public static function baseFieldDefinitions($entity_type) { 'property_constraints' => array( 'value' => array('Length' => array('max' => 255)), ), + 'translatable' => TRUE, ); $properties['uid'] = array( 'label' => t('User ID'), diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php b/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php index 77c0bbd5a63b4f1bf20c95e8a5d375395dfba848..338955c5846bc55850e896ec4d1b2abc30777215 100644 --- a/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php +++ b/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php @@ -15,11 +15,6 @@ */ class NodeTranslationUITest extends ContentTranslationUITest { - /** - * The title of the test node. - */ - protected $title; - /** * Modules to enable. * @@ -38,7 +33,6 @@ public static function getInfo() { function setUp() { $this->entityType = 'node'; $this->bundle = 'article'; - $this->title = $this->randomName(); parent::setUp(); $this->drupalPlaceBlock('system_help_block', array('region' => 'content')); } @@ -62,8 +56,7 @@ protected function getTranslatorPermissions() { * Overrides \Drupal\content_translation\Tests\ContentTranslationUITest::getNewEntityValues(). */ protected function getNewEntityValues($langcode) { - // Node title is not translatable yet, hence we use a fixed value. - return array('title' => $this->title) + parent::getNewEntityValues($langcode); + return array('title' => $this->randomName()) + parent::getNewEntityValues($langcode); } /** diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php index b397f2c03f455aed846744cbda10889770c451b8..e2b0d4f395a54c2d4c30921e038df39fee098cd2 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php @@ -9,6 +9,7 @@ use Drupal\Core\Language\Language; use Drupal\Core\TypedData\TranslatableInterface; +use Drupal\entity_test\Entity\EntityTestMulRev; /** * Tests entity translation. @@ -492,4 +493,42 @@ function testEntityTranslationAPI() { $this->assertEqual($field->getLangcode(), $langcode2, 'Field object has the expected langcode.'); } + /** + * Check that field translatability is handled properly. + */ + function testFieldDefinitions() { + // Check that field translatability can be altered to be enabled or disabled + // in field definitions. + $entity_type = 'entity_test_mulrev'; + $this->state->set('entity_test.field_definitions.translatable', array('name' => FALSE)); + $this->entityManager->clearCachedFieldDefinitions(); + $definitions = $this->entityManager->getFieldDefinitions($entity_type); + $this->assertFalse($definitions['name']['translatable'], 'Field translatability can be disabled programmatically.'); + + $this->state->set('entity_test.field_definitions.translatable', array('name' => TRUE)); + $this->entityManager->clearCachedFieldDefinitions(); + $definitions = $this->entityManager->getFieldDefinitions($entity_type); + $this->assertTrue($definitions['name']['translatable'], 'Field translatability can be enabled programmatically.'); + + // Check that field translatability is disabled by default. + $base_field_definitions = EntityTestMulRev::baseFieldDefinitions($entity_type); + $this->assertTrue(!isset($base_field_definitions['id']['translatable']), 'Translatability for the id field is not defined.'); + $this->assertFalse($definitions['id']['translatable'], 'Field translatability is disabled by default.'); + + // Check that entity ids and langcode fields cannot be translatable. + foreach (array('id', 'uuid', 'revision_id', 'type', 'langcode') as $name) { + $this->state->set('entity_test.field_definitions.translatable', array($name => TRUE)); + $this->entityManager->clearCachedFieldDefinitions(); + $message = format_string('Field %field cannot be translatable.', array('%field' => $name)); + + try { + $definitions = $this->entityManager->getFieldDefinitions($entity_type); + $this->fail($message); + } + catch (\LogicException $e) { + $this->pass($message); + } + } + } + } diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module index cc8800e0cb17a83edc29ae83840ad7352dc04928..e949e5035366929a5d03186e90265b7963c36588 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.module +++ b/core/modules/system/tests/modules/entity_test/entity_test.module @@ -72,6 +72,17 @@ function entity_test_entity_info_alter(&$info) { } } +/** + * Implements hook_entity_field_info_alter(). + */ +function entity_test_entity_field_info_alter(&$info, $entity_type) { + if ($entity_type == 'entity_test_mulrev' && ($names = \Drupal::state()->get('entity_test.field_definitions.translatable'))) { + foreach ($names as $name => $value) { + $info['definitions'][$name]['translatable'] = $value; + } + } +} + /** * Creates a new bundle for entity_test entities. * diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTranslationUITest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTranslationUITest.php index 20becd171d918c447e778a37674ce3f275c4b3a8..99594ee538435fb9b60c047aee0bc9ffa71ae2c8 100644 --- a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTranslationUITest.php +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTranslationUITest.php @@ -90,9 +90,9 @@ public function testTranslationUI() { // Make sure that no row was inserted for taxonomy vocabularies, which do // not have translations enabled. $rows = db_query('SELECT * FROM {content_translation}')->fetchAll(); - $this->assertEqual(2, count($rows)); - $this->assertEqual('taxonomy_term', $rows[0]->entity_type); - $this->assertEqual('taxonomy_term', $rows[1]->entity_type); + foreach ($rows as $row) { + $this->assertEqual('taxonomy_term', $row->entity_type); + } } /**