diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php index 6c8e48b..0b4372e 100644 --- a/core/lib/Drupal/Core/Entity/EntityManager.php +++ b/core/lib/Drupal/Core/Entity/EntityManager.php @@ -43,6 +43,8 @@ * multiple entity forms when the forms are similar. Defaults to * Drupal\Core\Entity\EntityFormController. * - label: The human-readable name of the type. + * - bundle_label: The human-readable name of the entity bundles, e.g. + * Vocabulary. * - label_callback: (optional) A function taking an entity and optional * langcode argument, and returning the label of the entity. If langcode is * omitted, the entity's default language is used. diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php b/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php index e0bbfd9..96b436e 100644 --- a/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php +++ b/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php @@ -18,6 +18,7 @@ * @Plugin( * id = "comment", * label = @Translation("Comment"), + * bundle_label = @Translation("Content type"), * module = "comment", * controller_class = "Drupal\comment\CommentStorageController", * render_controller_class = "Drupal\comment\CommentRenderController", diff --git a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php index 1fd8fc8..2aef56c 100644 --- a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php +++ b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php @@ -18,6 +18,7 @@ * @Plugin( * id = "node", * label = @Translation("Node"), + * bundle_label = @Translation("Content type"), * module = "node", * controller_class = "Drupal\node\NodeStorageController", * render_controller_class = "Drupal\node\NodeRenderController", diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php index 51468af..e129e87 100644 --- a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php @@ -18,6 +18,7 @@ * @Plugin( * id = "taxonomy_term", * label = @Translation("Taxonomy term"), + * bundle_label = @Translation("Vocabulary"), * module = "taxonomy", * controller_class = "Drupal\taxonomy\TermStorageController", * render_controller_class = "Drupal\taxonomy\TermRenderController", diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationSettingsTest.php b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationSettingsTest.php new file mode 100644 index 0000000..3dbae43 --- /dev/null +++ b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationSettingsTest.php @@ -0,0 +1,102 @@ + 'Entity Translation settings', + 'description' => 'Tests the entity translation settings UI.', + 'group' => 'Entity Translation UI', + ); + } + + /** + * Overrides \Drupal\simpletest\WebTestBase::setUp(). + */ + function setUp() { + parent::setUp(); + + // Setup two content types to test instances shared among different bundles. + $this->drupalCreateContentType(array('type' => 'article')); + $this->drupalCreateContentType(array('type' => 'page')); + + $admin_user = $this->drupalCreateUser(array('administer entity translation')); + $this->drupalLogin($admin_user); + } + + /** + * Tests that the settings UI works as expected. + */ + function testSettingsUI() { + // Test that by marking only an entity type and no bundle as translatable a + // form error is raised and the settings are not saved. + $edit = array('entity_types[comment]' => TRUE); + $this->assertSettings('comment', NULL, FALSE, $edit); + $this->assertTrue($this->xpath('//div[@id="messages"]//div[contains(@class, "error")]'), 'Enabling translation only for entity types generates a form error.'); + + // Test that by marking only a bundle and not the related entity type as + // translatable the settings are ignored. + $edit = array('settings[comment][comment_node_article][translatable]' => TRUE); + $this->assertSettings('comment', NULL, FALSE, $edit); + + // Test that by marking only a field as translatable and not the related + // entity type and bundle the settings are ignored. + $edit = array('settings[comment][comment_node_article][fields][comment_body]' => TRUE); + $this->assertSettings('comment', NULL, FALSE, $edit); + + // Test that by marking entity type and bundle as translatable the settings + // are stored. + $edit = array( + 'entity_types[comment]' => TRUE, + 'settings[comment][comment_node_article][translatable]' => TRUE, + ); + $this->assertSettings('comment', 'comment_node_article', TRUE, $edit); + + // Test that a field shared among different bundles can be enabled without + // needing to make all the related bundles translatable. + $edit = array( + 'settings[comment][comment_node_article][settings][language][langcode]' => 'current_interface', + 'settings[comment][comment_node_article][settings][language][language_hidden]' => FALSE, + 'settings[comment][comment_node_article][fields][comment_body]' => TRUE, + ); + $this->assertSettings('comment', 'comment_node_article', TRUE, $edit); + $field = field_info_field('comment_body'); + $this->assertTrue($field['translatable'], 'Comment body is translatable.'); + + // Test that language settings are correctly stored. + $language_configuration = language_get_default_configuration('comment', 'comment_node_article'); + $this->assertEqual($language_configuration['langcode'], 'current_interface', 'The default language for article comments is set to the currrent interface language.'); + $this->assertFalse($language_configuration['language_hidden'], 'The language selector for article comments is shown.'); + } + + /** + * Asserts that translatability has the expected value for the given bundle. + */ + protected function assertSettings($entity_type, $bundle, $enabled, $edit) { + $this->drupalPost('admin/config/regional/translation_entity', $edit, t('Save settings')); + $args = array('@entity_type' => $entity_type, '@bundle' => $bundle, '@enabled' => $enabled ? 'enabled' : 'disabled'); + $message = format_string('Translation for entity @entity_type (@bundle) is @enabled.', $args); + drupal_static_reset(); + return $this->assertEqual(translation_entity_enabled($entity_type, $bundle), $enabled, $message); + } + +} diff --git a/core/modules/translation_entity/translation_entity.admin.css b/core/modules/translation_entity/translation_entity.admin.css new file mode 100644 index 0000000..db1df56 --- /dev/null +++ b/core/modules/translation_entity/translation_entity.admin.css @@ -0,0 +1,20 @@ +/** + * @file + * Styles for the administration page. + */ + +.translation-entity-admin-settings-form table .bundle { + width: 25%; +} + +.translation-entity-admin-settings-form table .field { + width: 25%; +} + +.translation-entity-admin-settings-form table .translatable { + width: 10%; +} + +.translation-entity-admin-settings-form table .operations { + width: 40%; +} diff --git a/core/modules/translation_entity/translation_entity.admin.inc b/core/modules/translation_entity/translation_entity.admin.inc index bacba11..ccca415 100644 --- a/core/modules/translation_entity/translation_entity.admin.inc +++ b/core/modules/translation_entity/translation_entity.admin.inc @@ -8,6 +8,199 @@ use Drupal\Core\Entity\EntityInterface; /** + * Administration settings page callback. + */ +function translation_entity_admin_page() { + return drupal_get_form('translation_entity_admin_settings_form', translation_entity_supported()); +} + +/** + * Form builder for the administration settings form. + */ +function translation_entity_admin_settings_form(array $form, array $form_state, array $supported) { + $entity_info = entity_get_info(); + $labels = array(); + $default = array(); + + foreach ($supported as $entity_type) { + $labels[$entity_type] = isset($entity_info[$entity_type]['label']) ? $entity_info[$entity_type]['label'] : $entity_type; + $default[$entity_type] = translation_entity_enabled($entity_type) ? $entity_type : FALSE; + } + + $path = drupal_get_path('module', 'translation_entity'); + $form = array( + '#attached' => array( + 'css' => array($path . '/translation_entity.admin.css'), + 'js' => array($path . '/translation_entity.admin.js'), + ), + ); + + $translatable = t('Translatable'); + $form['entity_types'] = array( + '#title' => $translatable, + '#type' => 'checkboxes', + '#options' => $labels, + '#default_value' => $default, + ); + + $form['settings'] = array('#tree' => TRUE); + + foreach ($supported as $entity_type) { + $info = $entity_info[$entity_type]; + + $form['settings'][$entity_type] = array( + '#title' => $labels[$entity_type], + '#type' => 'item', + '#theme' => 'translation_entity_admin_settings_table', + '#bundle_label' => isset($info['bundle_keys']['label']) ? $info['bundle_keys']['label'] : $labels[$entity_type], + '#states' => array( + 'visible' => array( + ':input[name="entity_types[' . $entity_type . ']"]' => array('checked' => TRUE), + ), + ), + ); + + foreach (entity_get_bundles($entity_type) as $bundle) { + $form['settings'][$entity_type][$bundle]['translatable'] = array( + '#label' => isset($info['bundles'][$bundle]) ? $info['bundles'][$bundle]['label'] : $labels[$entity_type], + '#type' => 'checkbox', + '#default_value' => translation_entity_enabled($entity_type, $bundle), + ); + + $language_configuration = language_get_default_configuration($entity_type, $bundle); + $form['settings'][$entity_type][$bundle]['settings'] = array( + '#type' => 'item', + '#states' => array( + 'visible' => array( + ':input[name="settings[' . $entity_type . '][' . $bundle . '][translatable]"]' => array('checked' => TRUE), + ), + ), + 'language' => array( + '#type' => 'language_configuration', + '#entity_information' => array( + 'entity_type' => $entity_type, + 'bundle' => $bundle, + ), + '#default_value' => $language_configuration, + // Here we do not want the widget to be altered and hold also the + // "Enable translation" checkbox, which would be redundant. Hence we + // add this key to be able to skip alterations. + '#translation_entity_skip_alter' => TRUE, + ), + ); + + // @todo Exploit field definitions once all core entities and field types + // are migrated to the Entity Field API. + foreach (field_info_instances($entity_type, $bundle) as $field_name => $instance) { + $field = field_info_field($field_name); + $form['settings'][$entity_type][$bundle]['fields'][$field_name] = array( + '#label' => $instance['label'], + '#type' => 'checkbox', + '#default_value' => $field['translatable'], + ); + } + } + } + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save settings'), + ); + + return $form; +} + +/** + * Form validation handler for translation_entity_admin_settings_form(). + */ +function translation_entity_admin_settings_form_validate(array $form, array &$form_state) { + $settings = &$form_state['values']['settings']; + foreach (array_filter($form_state['values']['entity_types']) as $entity_type) { + $translatable = array_filter($settings[$entity_type], function($value) { return !empty($value['translatable']); }); + if (empty($translatable)) { + $t_args = array( + '%bundle' => $form['settings'][$entity_type]['#bundle_label'], + '%entity_type' => $form['settings'][$entity_type]['#title'], + ); + form_set_error('entity_types][' . $entity_type, t('At least one %bundle needs to be translatable to enable %entity_type translation.', $t_args)); + } + } +} + +/** + * Form submission handler for translation_entity_admin_settings_form(). + */ +function translation_entity_admin_settings_form_submit(array $form, array &$form_state) { + $entity_types = $form_state['values']['entity_types']; + $settings = &$form_state['values']['settings']; + + // If an entity type is not translatable all its bundles and fields must be + // marked as not translatable. Similarly, if a bundle is made non translatable + // all of its fields will be non translatable. + foreach ($settings as $entity_type => &$entity_settings) { + foreach ($entity_settings as $bundle => &$bundle_settings) { + $bundle_settings['translatable'] = $bundle_settings['translatable'] && $entity_types[$entity_type]; + if (!empty($bundle_settings['fields'])) { + foreach ($bundle_settings['fields'] as $field_name => $translatable) { + $bundle_settings['fields'][$field_name] = $translatable && $bundle_settings['translatable']; + } + } + } + } + + translation_entity_save_settings($settings); + drupal_set_message(t('Settings successfully updated.')); +} + +/** + * Theming function for an administration settings table. + */ +function theme_translation_entity_admin_settings_table(array $variables) { + $element = $variables['element']; + + $header = array( + array('data' => $element['#bundle_label'], 'class' => array('bundle')), + array('data' => t('Field'), 'class' => array('field')), + array('data' => t('Translatable'), 'class' => array('translatable')), + array('data' => t('Operations'), 'class' => array('operations')), + ); + + $rows = array(); + + foreach (element_children($element) as $bundle) { + $field_names = !empty($element[$bundle]['fields']) ? element_children($element[$bundle]['fields']) : array(); + + $rows[] = array( + 'data' => array( + array('data' => check_plain($element[$bundle]['translatable']['#label']), 'class' => array('bundle')), + array('data' => '', 'class' => array('field')), + array('data' => drupal_render($element[$bundle]['translatable']), 'class' => array('translatable')), + array('data' => drupal_render($element[$bundle]['settings']), 'class' => array('operations')), + ), + 'class' => array('bundle-settings'), + ); + + foreach ($field_names as $field_name) { + $field = &$element[$bundle]['fields'][$field_name]; + $rows[] = array( + 'data' => array( + array('data' => '', 'class' => array('bundle')), + array('data' => check_plain($field['#label']), 'class' => array('field')), + array('data' => drupal_render($field), 'class' => array('translatable')), + array('data' => '', 'class' => array('operations')), + ), + 'class' => array('field-settings'), + ); + } + } + + $output = theme('table', array('header' => $header, 'rows' => $rows)); + $output .= drupal_render_children($element); + return $output; +} + +/** * Form constructor for the confirmation of translatability switching. */ function translation_entity_translatable_form(array $form, array &$form_state, $field_name) { diff --git a/core/modules/translation_entity/translation_entity.admin.js b/core/modules/translation_entity/translation_entity.admin.js new file mode 100644 index 0000000..8793345 --- /dev/null +++ b/core/modules/translation_entity/translation_entity.admin.js @@ -0,0 +1,35 @@ +(function ($) { + +"use strict"; + +/** + * Makes field translatability inherit bundle translatability. + */ +Drupal.behaviors.translationEntity = { + attach: function (context) { + // Initially hide all field rows for non translatable bundles. + $('table .bundle-settings .translatable :input:not(:checked)').once('translation-entity-admin-hide', function() { + $(this).closest('.bundle-settings').nextUntil('.bundle-settings').hide(); + }); + + // When a bundle is made translatable all of its field instances should + // inherit this setting. Instead when it is made non translatable its field + // instances are hidden, since their translatability no longer matters. + $('table .bundle-settings .translatable :input').once('translation-entity-admin-bind', function() { + var $bundleTranslatable = $(this).click(function() { + var $bundleSettings = $bundleTranslatable.closest('.bundle-settings'); + var $fieldSettings = $bundleSettings.nextUntil('.bundle-settings'); + if ($bundleTranslatable.is(':checked')) { + $bundleSettings.find('.operations :input[name$="[language_hidden]"]').attr('checked', false); + $fieldSettings.find('.translatable :input').attr('checked', true); + $fieldSettings.show(); + } + else { + $fieldSettings.hide(); + } + }); + }); + } +}; + +})(jQuery); diff --git a/core/modules/translation_entity/translation_entity.info b/core/modules/translation_entity/translation_entity.info index 4a28def..3032a52 100644 --- a/core/modules/translation_entity/translation_entity.info +++ b/core/modules/translation_entity/translation_entity.info @@ -4,3 +4,4 @@ dependencies[] = language package = Core version = VERSION core = 8.x +configure = admin/config/regional/translation_entity diff --git a/core/modules/translation_entity/translation_entity.install b/core/modules/translation_entity/translation_entity.install index e66e544..abaaaaa 100644 --- a/core/modules/translation_entity/translation_entity.install +++ b/core/modules/translation_entity/translation_entity.install @@ -66,7 +66,8 @@ function translation_entity_install() { function translation_entity_enable() { $t_args = array( '!language_url' => url('admin/config/regional/language'), + '!settings_url' => url('admin/config/regional/translation_entity'), ); - $message = t('You just added content translation capabilities to your site. To exploit them be sure to enable at least two languages and enable translation for content types, taxonomy vocabularies, accounts and any other element whose content you wish to translate.', $t_args); + $message = t('You just added content translation capabilities to your site. To exploit them be sure to enable at least two languages and enable translation for content types, taxonomy vocabularies, accounts and any other element whose content you wish to translate.', $t_args); drupal_set_message($message, 'warning'); } diff --git a/core/modules/translation_entity/translation_entity.module b/core/modules/translation_entity/translation_entity.module index f57773b..0efc21d 100644 --- a/core/modules/translation_entity/translation_entity.module +++ b/core/modules/translation_entity/translation_entity.module @@ -39,6 +39,9 @@ function translation_entity_help($path, $arg) { $output .= '
' . t('The Entity Translation module makes a basic set of permissions available. Additional permissions are made available after translation is enabled for each translatable element.', array('@permissions' => url('admin/people/permissions', array('fragment' => 'module-translation_entity')))) . '
'; $output .= ''; return $output; + + case 'admin/config/regional/translation_entity': + return '

' . t('Setup your content translation settings for all the translatable elements on your website. This allows you to enable/disable and configure them at once. As soon as new content types, vocabularies and other translatable elements are available, you will be able to configure them here.') . '

'; } } @@ -157,12 +160,20 @@ function translation_entity_menu() { } } + $items['admin/config/regional/translation_entity'] = array( + 'title' => 'Content translation settings', + 'description' => 'Configure content translation for any translatable element.', + 'page callback' => 'translation_entity_admin_page', + 'access arguments' => array('administer entity translation'), + 'file' => 'translation_entity.admin.inc', + ); + $items['admin/config/regional/translation_entity/translatable/%'] = array( 'title' => 'Confirm change in translatability.', 'description' => 'Confirm page for changing field translatability.', 'page callback' => 'drupal_get_form', 'page arguments' => array('translation_entity_translatable_form', 5), - 'access arguments' => array('toggle field translatability'), + 'access arguments' => array('administer entity translation'), 'file' => 'translation_entity.admin.inc', ); @@ -342,6 +353,26 @@ function translation_entity_enabled($entity_type, $bundle = NULL, $skip_handler } /** + * Returns a list of supported entity types. + * + * @return array + * An array of entity type names. + */ +function translation_entity_supported() { + $supported = array(); + foreach (entity_get_info() as $entity_type => $info) { + // @todo Revisit this once all core entities are migrated to the Entity + // Field API and translation for configuration entities has been sorted + // out. + $entity_class = new ReflectionClass($info['class']); + if (!empty($info['fieldable']) && !$entity_class->implementsInterface('Drupal\Core\Config\Entity\ConfigEntityInterface')) { + $supported[$entity_type] = $entity_type; + } + } + return $supported; +} + +/** * Entity translation controller factory. * * @param string $entity_type @@ -394,9 +425,9 @@ function translation_entity_permission() { 'title' => t('Edit original values'), 'description' => t('Access the entity form in the original language.'), ), - 'toggle field translatability' => array( - 'title' => t('Toggle field translatability'), - 'description' => t('Toggle translatability of fields performing a bulk update.'), + 'administer entity translation' => array( + 'title' => t('Administer entity translation'), + 'description' => t('Configure translatability of entities and fields.'), ), 'translate any entity' => array( 'title' => t('Translate any entity'), @@ -598,7 +629,7 @@ function translation_entity_form_field_ui_field_edit_form_alter(array &$form, ar '#title' => $link_title, '#href' => $path, '#options' => array('query' => drupal_get_destination()), - '#access' => user_access('toggle field translatability'), + '#access' => user_access('administer entity translation'), ), ); } @@ -658,19 +689,20 @@ function translation_entity_enable_widget($entity_type, $bundle, array &$form, a * Processed language configuration element. */ function translation_entity_language_configuration_element_process(array $element, array &$form_state, array &$form) { - $form_state['translation_entity']['key'] = $element['#name']; - $context = $form_state['language'][$element['#name']]; - - $element['translation_entity'] = array( - '#type' => 'checkbox', - '#title' => t('Enable translation'), - '#default_value' => translation_entity_enabled($context['entity_type'], $context['bundle']), - '#element_validate' => array('translation_entity_language_configuration_element_validate'), - '#prefix' => '', - ); + if (empty($element['#translation_entity_skip_alter'])) { + $form_state['translation_entity']['key'] = $element['#name']; + $context = $form_state['language'][$element['#name']]; - $form['#submit'][] = 'translation_entity_language_configuration_element_submit'; + $element['translation_entity'] = array( + '#type' => 'checkbox', + '#title' => t('Enable translation'), + '#default_value' => translation_entity_enabled($context['entity_type'], $context['bundle']), + '#element_validate' => array('translation_entity_language_configuration_element_validate'), + '#prefix' => '', + ); + $form['#submit'][] = 'translation_entity_language_configuration_element_submit'; + } return $element; } @@ -715,3 +747,63 @@ function translation_entity_language_configuration_element_submit(array $form, a menu_router_rebuild(); } } + +/** + * Stores entity translation settings. + * + * @param array $settings + * An associative array of settings keyed by entity type and bundle. At bundle + * level the following keys are available: + * - translatable: The bundle translatability status. + * - settings: An array of language configuration settings as defined by + * language_save_default_configuration(). + * - fields: An associative array with field names as keys and a boolean as + * value, indicating field translatability. + */ +function translation_entity_save_settings($settings) { + $fields = array(); + + foreach ($settings as $entity_type => $entity_settings) { + foreach ($entity_settings as $bundle => $bundle_settings) { + // Update bundle translatability. + translation_entity_set_config($entity_type, $bundle, 'enabled', $bundle_settings['translatable']); + // Update language settings. + language_save_default_configuration($entity_type, $bundle, $bundle_settings['settings']['language']); + // Collapse field settings since here we have per instance settings, but + // translatability has per-field scope. We assume that all the field + // instances have the same value. + if (!empty($bundle_settings['fields'])) { + 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[$field_name] = $translatable || !empty($fields[$field_name]); + } + } + // @todo Store non-configurable field settings to be able to alter their + // definition afterwards. + } + } + + // Update field translatability. + foreach ($fields as $field_name => $translatable) { + $field = field_info_field($field_name); + $field['translatable'] = $translatable; + field_update_field($field); + } + + // Ensure entity and menu router information are correctly rebuilt. + entity_info_cache_clear(); + menu_router_rebuild(); +} + +/** + * Implemements hook_theme(). + */ +function translation_entity_theme() { + return array( + 'translation_entity_admin_settings_table' => array( + 'render element' => 'element', + 'file' => 'translation_entity.admin.inc', + ), + ); +}