Commit c77cee1c authored by webchick's avatar webchick
Browse files

Issue #1810386 by plach, YesCT, Bojhan, bforchhammer, Gábor Hojtsy, falcon03:...

Issue #1810386 by plach, YesCT, Bojhan, bforchhammer, Gábor Hojtsy, falcon03: Create workflow to setup multilingual for entity types, bundles and fields.
parent 22883148
......@@ -44,6 +44,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.
......
......@@ -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",
......
/**
* @file
* Styles for the content language administration page.
*/
#language-content-settings-form table .bundle {
width: 25%;
}
#language-content-settings-form table td.bundle {
font-weight: bold;
}
#language-content-settings-form table .operations {
width: 75%;
}
......@@ -1027,3 +1027,171 @@ function language_negotiation_configure_browser_delete_form_submit($form, &$form
}
$form_state['redirect'] = 'admin/config/regional/language/detection/browser';
}
/**
* Returns the content language settings form.
*/
function language_content_settings_page() {
return drupal_get_form('language_content_settings_form', language_entity_supported());
}
/**
* Form constructor for the content language settings form.
*
* @param array $supported
* Entity types with language support.
*
* @see language_content_settings_form_submit()
*
* @ingroup forms
*/
function language_content_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] = FALSE;
// Check whether we have any custom setting.
foreach (entity_get_bundles($entity_type) as $bundle) {
$conf = language_get_default_configuration($entity_type, $bundle);
if (empty($conf['language_hidden']) || $conf['langcode'] != 'site_default') {
$default[$entity_type] = $entity_type;
}
$language_configuration[$entity_type][$bundle] = $conf;
}
}
$path = drupal_get_path('module', 'language');
$form = array(
'#labels' => $labels,
'#attached' => array(
'css' => array($path . '/language.admin.css'),
),
);
$form['entity_types'] = array(
'#title' => t('Custom language settings'),
'#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' => 'container',
'#theme' => 'language_content_settings_table',
'#bundle_label' => isset($info['bundle_label']) ? $info['bundle_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]['settings'] = array(
'#type' => 'item',
'#label' => isset($info['bundles'][$bundle]) ? $info['bundles'][$bundle]['label'] : $labels[$entity_type],
'language' => array(
'#type' => 'language_configuration',
'#entity_information' => array(
'entity_type' => $entity_type,
'bundle' => $bundle,
),
'#default_value' => $language_configuration[$entity_type][$bundle],
),
);
}
}
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Save'),
);
return $form;
}
/**
* Implements hook_preprocess_HOOK() for theme_language_content_settings_table().
*/
function template_preprocess_language_content_settings_table(&$variables) {
// Add a render element representing the bundle language settings table.
$element = $variables['element'];
$header = array(
array(
'data' => $element['#bundle_label'],
'class' => array('bundle'),
),
array(
'data' => t('Configuration'),
'class' => array('operations'),
),
);
$rows = array();
foreach (element_children($element) as $bundle) {
$rows[$bundle] = array(
'data' => array(
array(
'data' => array(
'#prefix' => '<label>',
'#suffix' => '</label>',
'#markup' => check_plain($element[$bundle]['settings']['#label']),
),
'class' => array('bundle'),
),
array(
'data' => $element[$bundle]['settings'],
'class' => array('operations'),
),
),
'class' => array('bundle-settings'),
);
}
$variables['build'] = array(
'#title' => $element['#title'],
'#header' => $header,
'#rows' => $rows,
'#theme' => 'table',
);
}
/**
* Returns HTML for an administration settings table.
*
* @param array $variables
* An associative array containing:
* - build: A render element representing a table of bundle content language
* settings for a particular entity type.
*
* @ingroup themable
*/
function theme_language_content_settings_table($variables) {
return '<h4>' . $variables['build']['#title'] . '</h4>' . drupal_render($variables['build']);
}
/**
* Form submission handler for language_content_settings_form().
*/
function language_content_settings_form_submit(array $form, array &$form_state) {
$entity_types = $form_state['values']['entity_types'];
$settings = &$form_state['values']['settings'];
foreach ($settings as $entity_type => $entity_settings) {
foreach ($entity_settings as $bundle => $bundle_settings) {
language_save_default_configuration($entity_type, $bundle, $bundle_settings['settings']['language']);
}
}
drupal_set_message(t('Settings successfully updated.'));
}
......@@ -52,6 +52,9 @@ function language_help($path, $arg) {
return '<p>' . t('With multiple languages enabled, registered users can select their preferred language and authors can assign a specific language to content.') . '</p>';
}
break;
case 'admin/config/regional/content-language':
return t('Change language settings for <em>content types</em>, <em>taxonomy vocabularies</em>, <em>user profiles</em>, or any other supported element on your site. By default, language settings hide the language selector and the language is the site\'s default language.');
}
}
......@@ -150,6 +153,15 @@ function language_menu() {
'type' => MENU_VISIBLE_IN_BREADCRUMB,
);
// Content language settings.
$items['admin/config/regional/content-language'] = array(
'title' => 'Content language settings',
'description' => 'Configure content language support for any multilingual element.',
'page callback' => 'language_content_settings_page',
'access arguments' => array('administer languages'),
'file' => 'language.admin.inc',
);
return $items;
}
......@@ -187,9 +199,33 @@ function language_theme() {
'render element' => 'form',
'file' => 'language.admin.inc',
),
'language_content_settings_table' => array(
'render element' => 'element',
'file' => 'language.admin.inc',
),
);
}
/**
* Returns a list of supported entity types.
*
* @return array
* An array of entity type names.
*/
function language_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 language support for configuration entities has been
// sorted out.
$entity_class = new ReflectionClass($info['class']);
if ($info['fieldable'] && !$entity_class->implementsInterface('Drupal\Core\Config\Entity\ConfigEntityInterface')) {
$supported[$entity_type] = $entity_type;
}
}
return $supported;
}
/**
* Implements hook_element_info_alter().
*/
......
......@@ -18,6 +18,7 @@
* @Plugin(
* id = "node",
* label = @Translation("Content"),
* bundle_label = @Translation("Content type"),
* module = "node",
* controller_class = "Drupal\node\NodeStorageController",
* render_controller_class = "Drupal\node\NodeRenderController",
......
......@@ -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",
......
<?php
/**
* @file
* Contains Drupal\translation_entity\Tests\EntityTranslationSettingsTest.
*/
namespace Drupal\translation_entity\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests the Entity Test Translation UI.
*/
class EntityTranslationSettingsTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('language', 'translation_entity', 'comment');
public static function getInfo() {
return array(
'name' => 'Entity Translation settings',
'description' => 'Tests the entity translation settings UI.',
'group' => 'Entity Translation UI',
);
}
function setUp() {
parent::setUp();
// Set up two content types to test field instances shared between different
// bundles.
$this->drupalCreateContentType(array('type' => 'article'));
$this->drupalCreateContentType(array('type' => 'page'));
$admin_user = $this->drupalCreateUser(array('administer languages', 'administer entity translation'));
$this->drupalLogin($admin_user);
}
/**
* Tests that the settings UI works as expected.
*/
function testSettingsUI() {
// Test that the translation settings are ignored if the bundle is marked
// translatable but the entity type is not.
$edit = array('settings[comment][comment_node_article][translatable]' => TRUE);
$this->assertSettings('comment', NULL, 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 = array('settings[comment][comment_node_article][fields][comment_body]' => TRUE);
$this->assertSettings('comment', NULL, 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 = array(
'entity_types[comment]' => TRUE,
'settings[comment][comment_node_article][translatable]' => TRUE,
);
$this->assertSettings('comment', 'comment_node_article', FALSE, $edit);
$xpath_err = '//div[@id="messages"]//div[contains(@class, "error")]';
$this->assertTrue($this->xpath($xpath_err), 'Enabling translation only for entity bundles generates a form 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 = array(
'entity_types[comment]' => TRUE,
'settings[comment][comment_node_article][settings][language][langcode]' => LANGUAGE_NOT_SPECIFIED,
'settings[comment][comment_node_article][settings][language][language_hidden]' => TRUE,
'settings[comment][comment_node_article][translatable]' => TRUE,
'settings[comment][comment_node_article][fields][comment_body]' => TRUE,
);
$this->assertSettings('comment', 'comment_node_article', FALSE, $edit);
$this->assertTrue($this->xpath($xpath_err), 'Enabling translation with a fixed non-configurable language generates a form error.');
// Test that a field shared among different bundles can be enabled without
// needing to make all the related bundles translatable.
$edit = array(
'entity_types[comment]' => TRUE,
'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][translatable]' => TRUE,
'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 current 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.
*
* @param string $entity_type
* The entity type for which to check translatibility.
* @param string $bundle
* The bundle for which to check translatibility.
* @param boolean $enabled
* TRUE if translatibility should be enabled, FALSE otherwise.
* @param array $edit
* An array of values to submit to the entity translation settings page.
*
* @return boolean
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertSettings($entity_type, $bundle, $enabled, $edit) {
$this->drupalPost('admin/config/regional/content-language', $edit, t('Save'));
$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);
}
}
/**
* @file
* Styles for the content language administration page.
*/
#language-content-settings-form table .bundle {
width: 24%;
}
#language-content-settings-form table td.bundle {
font-weight: bold;
}
#language-content-settings-form table .field {
width: 24%;
padding-left: 3em;
}
#language-content-settings-form table .field label {
font-weight: normal;
}
#language-content-settings-form table .translatable {
width: 1%;
}
#language-content-settings-form table .operations {
width: 75%;
}
......@@ -7,6 +7,180 @@
use Drupal\Core\Entity\EntityInterface;
/**
* Implements hook_form_FORM_ID_alter() for language_content_settings_form().
*
* @see translation_entity_form_language_content_settings_form_alter()
*/
function _translation_entity_form_language_content_settings_form_alter(array &$form, array &$form_state) {
// Inject into the content language settings the translation settings if the
// user has the required permission.
if (!user_access('administer entity translation')) {
return;
}
$default = $form['entity_types']['#default_value'];
foreach ($default as $entity_type => $enabled) {
$default[$entity_type] = $enabled || translation_entity_enabled($entity_type) ? $entity_type : FALSE;
}
$form['entity_types']['#default_value'] = $default;
$path = drupal_get_path('module', 'translation_entity');
$form['#attached']['css'][] = $path . '/translation_entity.admin.css';
$form['#attached']['js'][] = $path . '/translation_entity.admin.js';
foreach ($form['#labels'] as $entity_type => $label) {
foreach (entity_get_bundles($entity_type) as $bundle) {
$form['settings'][$entity_type][$bundle]['translatable'] = array(
'#type' => 'checkbox',
'#default_value' => translation_entity_enabled($entity_type, $bundle),
);
// 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.
$form['settings'][$entity_type][$bundle]['settings']['language']['#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['#validate'][] = 'translation_entity_form_language_content_settings_validate';
$form['#submit'][] = 'translation_entity_form_language_content_settings_submit';
}
/**
* Form validation handler for translation_entity_admin_settings_form().
*
* @see translation_entity_admin_settings_form_submit()
*/
function translation_entity_form_language_content_settings_validate(array $form, array &$form_state) {
$settings = &$form_state['values']['settings'];
foreach ($settings as $entity_type => $entity_settings) {
foreach ($entity_settings as $bundle => $bundle_settings) {
if (!empty($bundle_settings['translatable'])) {
$name = "settings][$entity_type][$bundle][translatable";
$translatable_fields = isset($settings[$entity_type][$bundle]['fields']) ? array_filter($settings[$entity_type][$bundle]['fields']) : FALSE;
if (empty($translatable_fields)) {
$t_args = array('%bundle' => $form['settings'][$entity_type][$bundle]['settings']['#label']);
form_set_error($name, t('At least one field needs to be translatable to enable %bundle for translation.', $t_args));
}
$values = $bundle_settings['settings']['language'];
if (language_is_locked($values['langcode']) && $values['language_hidden']) {
foreach (language_list(LANGUAGE_LOCKED) as $language) {
$locked_languages[] = $language->name;
}
form_set_error($name, t('Translation is not supported if language is always one of: @locked_languages', array('@locked_languages' => implode(', ', $locked_languages))));
}
}
}
}
}
/**
* Form submission handler for translation_entity_admin_settings_form().
*
* @see translation_entity_admin_settings_form_validate()
*/
function translation_entity_form_language_content_settings_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 non-translatable. Similarly, if a bundle is made non-translatable
// all of its fields will be not 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_update_field_translatability($settings);
drupal_set_message(t('Settings successfully updated.'));
}
/**
* 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, which is a bool.
* - 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.
*
* @todo Remove this migration entirely once the Field API is converted to the
* Entity Field API.
*/
function _translation_entity_update_field_translatability($settings) {
$fields = array();
foreach ($settings as $entity_type => $entity_settings) {
foreach ($entity_settings as $bundle => $bundle_settings) {
// 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.
}
}
$operations = array();
foreach ($fields as $field_name => $translatable) {
$field = field_info_field($field_name);
if ($field['translatable'] != $translatable) {
// If a field is untranslatable, it can have no data except under
// LANGUAGE_NOT_SPECIFIED. Thus we need a field to be translatable before