diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php index e34adb6..8a31e3f 100644 --- a/core/lib/Drupal/Core/Entity/EntityManager.php +++ b/core/lib/Drupal/Core/Entity/EntityManager.php @@ -102,6 +102,7 @@ * Elements: * - bundle: The name of the property that contains the name of the bundle * object. + * - label: The human-readable name of the entity bundles, e.g. Vocabulary. * - bundles: An array describing all bundles for this object type. Keys are * bundle machine names, as found in the objects' 'bundle' property * (defined in the 'entity_keys' entry for the entity type in the 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..d919e49 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 @@ -35,6 +35,9 @@ * "label" = "subject", * "uuid" = "uuid" * }, + * bundle_keys = { + * "label" = "Node type" + * }, * view_modes = { * "full" = { * "label" = "Full comment", 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..a7b7ff0 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 @@ -37,7 +37,8 @@ * "uuid" = "uuid" * }, * bundle_keys = { - * "bundle" = "type" + * "bundle" = "type", + * "label" = "Content type" * }, * view_modes = { * "full" = { 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..c5932b0 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 @@ -35,7 +35,8 @@ * "uuid" = "uuid" * }, * bundle_keys = { - * "bundle" = "machine_name" + * "bundle" = "machine_name", + * "label" = "Vocabulary" * }, * view_modes = { * "full" = { 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..c3bf85e --- /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..72866f5 100644 --- a/core/modules/translation_entity/translation_entity.admin.inc +++ b/core/modules/translation_entity/translation_entity.admin.inc @@ -8,6 +8,196 @@ 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; + } + + $translatable = t('Translatable'); + $form['entity_types'] = array( + '#title' => $translatable, + '#type' => 'checkboxes', + '#options' => $labels, + '#default_value' => $default, + '#attached' => array( + 'css' => array(drupal_get_path('module', 'translation_entity') . '/translation_entity.admin.css'), + 'js' => array(drupal_get_path('module', 'translation_entity') . '/translation_entity.admin.js'), + ), + ); + + $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, + '#translation_entity_skip' => 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 non translatable all its bundles and fields must be + // marked as non 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'), + ); + } + } + + $element['#children'] = theme('table', array('header' => $header, 'rows' => $rows)); + + // We now need to render the parent element to ensure states are properly + // processed. + unset($element['#theme']); + unset($element['#theme_wrappers']); + return drupal_render($element); +} + +/** * 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..5c9e9a8 --- /dev/null +++ b/core/modules/translation_entity/translation_entity.admin.js @@ -0,0 +1,33 @@ +(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 $checkbox = $(this).click(function() { + var $checkboxes = $checkbox.closest('.bundle-settings').nextUntil('.bundle-settings'); + if ($checkbox.is(':checked')) { + $checkboxes.find('.translatable :input').attr('checked', true); + $checkboxes.show(); + } + else { + $checkboxes.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.module b/core/modules/translation_entity/translation_entity.module index f57773b..7fd7e75 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 translation settings for all the different types on your website, this allows you to enable/disable and configure it at once. Newly created content types, vocabularies, etc will be added 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'), @@ -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'])) { + $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($existing, $type, $theme, $path) { + return array( + 'translation_entity_admin_settings_table' => array( + 'render element' => 'element', + 'file' => 'translation_entity.admin.inc', + ), + ); +}