Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,20 @@
# Schema for the Content Translation module.
field.field.*.*.*.third_party.content_translation:
type: mapping
label: 'Content translation field settings'
mapping:
translation_sync:
type: sequence
label: 'Field properties for which to synchronize translations'
sequence:
type: string
label: 'Field column for which to synchronize translations'
language.content_settings.*.*.third_party.content_translation:
type: mapping
label: 'Content translation content settings'
mapping:
enabled:
type: boolean
label: 'Content translation enabled'

View file

@ -0,0 +1,9 @@
# Schema for the views plugins of the Content Translation module.
views.field.content_translation_link:
type: views_field
label: 'Content translation link'
mapping:
text:
type: label
label: 'Text to display'

View file

@ -0,0 +1,371 @@
<?php
/**
* @file
* The content translation administration forms.
*/
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Render\Element;
/**
* Returns a form element to configure field synchronization.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field
* A field definition object.
*
* @return array
* A form element to configure field synchronization.
*/
function content_translation_field_sync_widget(FieldDefinitionInterface $field) {
// No way to store field sync information on this field.
if (!($field instanceof ThirdPartySettingsInterface)) {
return array();
}
$element = array();
$definition = \Drupal::service('plugin.manager.field.field_type')->getDefinition($field->getType());
$column_groups = $definition['column_groups'];
if (!empty($column_groups) && count($column_groups) > 1) {
$options = array();
$default = array();
foreach ($column_groups as $group => $info) {
$options[$group] = $info['label'];
$default[$group] = !empty($info['translatable']) ? $group : FALSE;
}
$settings = array('dependent_selectors' => array('instance[third_party_settings][content_translation][translation_sync]' => array('file')));
$default = $field->getThirdPartySetting('content_translation', 'translation_sync', $default);
$element = array(
'#type' => 'checkboxes',
'#title' => t('Translatable elements'),
'#options' => $options,
'#default_value' => $default,
'#attached' => array(
'library' => array(
'content_translation/drupal.content_translation.admin',
),
'drupalSettings' => [
'contentTranslationDependentOptions' => $settings,
],
),
);
}
return $element;
}
/**
* (proxied) Implements hook_form_FORM_ID_alter().
*/
function _content_translation_form_language_content_settings_form_alter(array &$form, FormStateInterface $form_state) {
// Inject into the content language settings the translation settings if the
// user has the required permission.
if (!\Drupal::currentUser()->hasPermission('administer content translation')) {
return;
}
$content_translation_manager = \Drupal::service('content_translation.manager');
$default = $form['entity_types']['#default_value'];
foreach ($default as $entity_type_id => $enabled) {
$default[$entity_type_id] = $enabled || $content_translation_manager->isEnabled($entity_type_id) ? $entity_type_id : FALSE;
}
$form['entity_types']['#default_value'] = $default;
$form['#attached']['library'][] = 'content_translation/drupal.content_translation.admin';
$dependent_options_settings = array();
$entity_manager = Drupal::entityManager();
foreach ($form['#labels'] as $entity_type_id => $label) {
$entity_type = $entity_manager->getDefinition($entity_type_id);
$storage_definitions = $entity_type instanceof ContentEntityTypeInterface ? $entity_manager->getFieldStorageDefinitions($entity_type_id) : array();
$entity_type_translatable = $content_translation_manager->isSupported($entity_type_id);
foreach (entity_get_bundles($entity_type_id) as $bundle => $bundle_info) {
// 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. Alter the title and display the message
// about UI integration.
$form['settings'][$entity_type_id][$bundle]['settings']['language']['#content_translation_skip_alter'] = TRUE;
if (!$entity_type_translatable) {
$form['settings'][$entity_type_id]['#title'] = t('@label (Translation is not supported).', array('@label' => $entity_type->getLabel()));
continue;
}
$fields = $entity_manager->getFieldDefinitions($entity_type_id, $bundle);
if ($fields) {
foreach ($fields as $field_name => $definition) {
if (!empty($storage_definitions[$field_name]) && _content_translation_is_field_translatability_configurable($entity_type, $storage_definitions[$field_name])) {
$form['settings'][$entity_type_id][$bundle]['fields'][$field_name] = array(
'#label' => $definition->getLabel(),
'#type' => 'checkbox',
'#default_value' => $definition->isTranslatable(),
);
// Display the column translatability configuration widget.
$column_element = content_translation_field_sync_widget($definition);
if ($column_element) {
$form['settings'][$entity_type_id][$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_id}][{$bundle}][columns][{$field_name}]"] = array('file');
}
}
}
}
if (!empty($form['settings'][$entity_type_id][$bundle]['fields'])) {
// Only show the checkbox to enable translation if the bundles in the
// entity might have fields and if there are fields to translate.
$form['settings'][$entity_type_id][$bundle]['translatable'] = array(
'#type' => 'checkbox',
'#default_value' => $content_translation_manager->isEnabled($entity_type_id, $bundle),
);
}
}
}
}
$settings = array('dependent_selectors' => $dependent_options_settings);
$form['#attached']['drupalSettings']['contentTranslationDependentOptions'] = $settings;
$form['#validate'][] = 'content_translation_form_language_content_settings_validate';
$form['#submit'][] = 'content_translation_form_language_content_settings_submit';
}
/**
* Checks whether translatability should be configurable for a field.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $definition
* The field storage definition.
*
* @return bool
* TRUE if field translatability can be configured, FALSE otherwise.
*
* @internal
*/
function _content_translation_is_field_translatability_configurable(EntityTypeInterface $entity_type, FieldStorageDefinitionInterface $definition) {
// Allow to configure only fields supporting multilingual storage. We skip our
// own fields as they are always translatable. Additionally we skip a set of
// well-known fields implementing entity system business logic.
return
$definition->isTranslatable() &&
$definition->getProvider() != 'content_translation' &&
!in_array($definition->getName(), [$entity_type->getKey('langcode'), $entity_type->getKey('default_langcode'), 'revision_translation_affected']);
}
/**
* (proxied) Implements hook_preprocess_HOOK();
*/
function _content_translation_preprocess_language_content_settings_table(&$variables) {
// Alter the 'build' variable injecting the translation settings if the user
// has the required permission.
if (!\Drupal::currentUser()->hasPermission('administer content translation')) {
return;
}
$element = $variables['element'];
$build = &$variables['build'];
array_unshift($build['#header'], array('data' => t('Translatable'), 'class' => array('translatable')));
$rows = array();
foreach (Element::children($element) as $bundle) {
$field_names = !empty($element[$bundle]['fields']) ? Element::children($element[$bundle]['fields']) : array();
if (!empty($element[$bundle]['translatable'])) {
$checkbox_id = $element[$bundle]['translatable']['#id'];
}
$rows[$bundle] = $build['#rows'][$bundle];
if (!empty($element[$bundle]['translatable'])) {
$translatable = array(
'data' => $element[$bundle]['translatable'],
'class' => array('translatable'),
);
array_unshift($rows[$bundle]['data'], $translatable);
$rows[$bundle]['data'][1]['data']['#prefix'] = '<label for="' . $checkbox_id . '">';
}
else {
$translatable = array(
'data' => t('N/A'),
'class' => array('untranslatable'),
);
array_unshift($rows[$bundle]['data'], $translatable);
}
foreach ($field_names as $field_name) {
$field_element = &$element[$bundle]['fields'][$field_name];
$rows[] = array(
'data' => array(
array(
'data' => drupal_render($field_element),
'class' => array('translatable'),
),
array(
'data' => array(
'#prefix' => '<label for="' . $field_element['#id'] . '">',
'#suffix' => '</label>',
'bundle' => array(
'#prefix' => '<span class="visually-hidden">',
'#suffix' => '</span> ',
'#markup' => SafeMarkup::checkPlain($element[$bundle]['settings']['#label']),
),
'field' => array(
'#markup' => SafeMarkup::checkPlain($field_element['#label']),
),
),
'class' => array('field'),
),
array(
'data' => '',
'class' => array('operations'),
),
),
'class' => array('field-settings'),
);
if (!empty($element[$bundle]['columns'][$field_name])) {
$column_element = &$element[$bundle]['columns'][$field_name];
foreach (Element::children($column_element) as $key) {
$column_label = $column_element[$key]['#title'];
unset($column_element[$key]['#title']);
$rows[] = array(
'data' => array(
array(
'data' => drupal_render($column_element[$key]),
'class' => array('translatable'),
),
array(
'data' => array(
'#prefix' => '<label for="' . $column_element[$key]['#id'] . '">',
'#suffix' => '</label>',
'bundle' => array(
'#prefix' => '<span class="visually-hidden">',
'#suffix' => '</span> ',
'#markup' => SafeMarkup::checkPlain($element[$bundle]['settings']['#label']),
),
'field' => array(
'#prefix' => '<span class="visually-hidden">',
'#suffix' => '</span> ',
'#markup' => SafeMarkup::checkPlain($field_element['#label']),
),
'columns' => array(
'#markup' => SafeMarkup::checkPlain($column_label),
),
),
'class' => array('column'),
),
array(
'data' => '',
'class' => array('operations'),
),
),
'class' => array('column-settings'),
);
}
}
}
}
$build['#rows'] = $rows;
}
/**
* Form validation handler for content_translation_admin_settings_form().
*
* @see content_translation_admin_settings_form_submit()
*/
function content_translation_form_language_content_settings_validate(array $form, FormStateInterface $form_state) {
$settings = &$form_state->getValue('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_state->setErrorByName($name, t('At least one field needs to be translatable to enable %bundle for translation.', $t_args));
}
$values = $bundle_settings['settings']['language'];
if (empty($values['language_alterable']) && \Drupal::languageManager()->isLanguageLocked($values['langcode'])) {
foreach (\Drupal::languageManager()->getLanguages(LanguageInterface::STATE_LOCKED) as $language) {
$locked_languages[] = $language->getName();
}
$form_state->setErrorByName($name, t('Translation is not supported if language is always one of: @locked_languages', array('@locked_languages' => implode(', ', $locked_languages))));
}
}
}
}
}
/**
* Form submission handler for content_translation_admin_settings_form().
*
* @see content_translation_admin_settings_form_validate()
*/
function content_translation_form_language_content_settings_submit(array $form, FormStateInterface $form_state) {
$entity_types = $form_state->getValue('entity_types');
$settings = &$form_state->getValue('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_id => &$entity_settings) {
foreach ($entity_settings as $bundle => &$bundle_settings) {
$fields = \Drupal::entityManager()->getFieldDefinitions($entity_type_id, $bundle);
if (!empty($bundle_settings['translatable'])) {
$bundle_settings['translatable'] = $bundle_settings['translatable'] && $entity_types[$entity_type_id];
}
if (!empty($bundle_settings['fields'])) {
foreach ($bundle_settings['fields'] as $field_name => $translatable) {
$translatable = $translatable && $bundle_settings['translatable'];
// If we have column settings and no column is translatable, no point
// in making the field translatable.
if (isset($bundle_settings['columns'][$field_name]) && !array_filter($bundle_settings['columns'][$field_name])) {
$translatable = FALSE;
}
$field_config = $fields[$field_name]->getConfig($bundle);
if ($field_config->isTranslatable() != $translatable) {
$field_config
->setTranslatable($translatable)
->save();
}
}
}
if (isset($bundle_settings['translatable'])) {
// Store whether a bundle has translation enabled or not.
\Drupal::service('content_translation.manager')->setEnabled($entity_type_id, $bundle, $bundle_settings['translatable']);
// Save translation_sync settings.
if (!empty($bundle_settings['columns'])) {
foreach ($bundle_settings['columns'] as $field_name => $column_settings) {
$field_config = $fields[$field_name]->getConfig($bundle);
if ($field_config->isTranslatable()) {
$field_config->setThirdPartySetting('content_translation', 'translation_sync', $column_settings);
}
// If the field does not have translatable enabled we need to reset
// the sync settings to their defaults.
else {
$field_config->unsetThirdPartySetting('content_translation', 'translation_sync');
}
$field_config->save();
}
}
}
}
}
// Ensure entity and menu router information are correctly rebuilt.
\Drupal::entityManager()->clearCachedDefinitions();
\Drupal::service('router.builder')->setRebuildNeeded();
}

View file

@ -0,0 +1,125 @@
/**
* @file
* Content Translation admin behaviors.
*/
(function ($, Drupal, drupalSettings) {
"use strict";
/**
* Forces applicable options to be checked as translatable.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.contentTranslationDependentOptions = {
attach: function (context) {
var $context = $(context);
var options = drupalSettings.contentTranslationDependentOptions;
var $fields;
var dependent_columns;
function fieldsChangeHandler($fields, dependent_columns) {
return function (e) {
Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependent_columns, $(e.target));
};
}
// We're given a generic name to look for so we find all inputs containing
// that name and copy over the input values that require all columns to be
// translatable.
if (options.dependent_selectors) {
for (var field in options.dependent_selectors) {
if (options.dependent_selectors.hasOwnProperty(field)) {
$fields = $context.find('input[name^="' + field + '"]');
dependent_columns = options.dependent_selectors[field];
$fields.on('change', fieldsChangeHandler($fields, dependent_columns));
Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependent_columns);
}
}
}
},
check: function ($fields, dependent_columns, $changed) {
var $element = $changed;
var column;
function filterFieldsList(index, field) {
return $(field).val() === column;
}
// A field that has many different translatable parts can also define one
// or more columns that require all columns to be translatable.
for (var index in dependent_columns) {
if (dependent_columns.hasOwnProperty(index)) {
column = dependent_columns[index];
if (!$changed) {
$element = $fields.filter(filterFieldsList);
}
if ($element.is('input[value="' + column + '"]:checked')) {
$fields.prop('checked', true)
.not($element).prop('disabled', true);
}
else {
$fields.prop('disabled', false);
}
}
}
}
};
/**
* Makes field translatability inherit bundle translatability.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.contentTranslation = {
attach: function (context) {
// Initially hide all field rows for non translatable bundles and all column
// rows for non translatable fields.
$(context).find('table .bundle-settings .translatable :input').once('translation-entity-admin-hide').each(function () {
var $input = $(this);
var $bundleSettings = $input.closest('.bundle-settings');
if (!$input.is(':checked')) {
$bundleSettings.nextUntil('.bundle-settings').hide();
}
else {
$bundleSettings.nextUntil('.bundle-settings', '.field-settings').find('.translatable :input:not(:checked)').closest('.field-settings').nextUntil(':not(.column-settings)').hide();
}
});
// When a bundle is made translatable all of its fields should inherit
// this setting. Instead when it is made non translatable its fields are
// hidden, since their translatability no longer matters.
$('body').once('translation-entity-admin-bind').on('click', 'table .bundle-settings .translatable :input', function (e) {
var $target = $(e.target);
var $bundleSettings = $target.closest('.bundle-settings');
var $settings = $bundleSettings.nextUntil('.bundle-settings');
var $fieldSettings = $settings.filter('.field-settings');
if ($target.is(':checked')) {
$bundleSettings.find('.operations :input[name$="[language_alterable]"]').prop('checked', true);
$fieldSettings.find('.translatable :input').prop('checked', true);
$settings.show();
}
else {
$settings.hide();
}
})
.on('click', 'table .field-settings .translatable :input', function (e) {
var $target = $(e.target);
var $fieldSettings = $target.closest('.field-settings');
var $columnSettings = $fieldSettings.nextUntil('.field-settings, .bundle-settings');
if ($target.is(':checked')) {
$columnSettings.show();
}
else {
$columnSettings.hide();
}
});
}
};
})(jQuery, Drupal, drupalSettings);

View file

@ -0,0 +1,9 @@
name: 'Content Translation'
type: module
description: 'Allows users to translate content entities.'
dependencies:
- language
package: Multilingual
version: VERSION
core: 8.x
configure: language.content_settings_page

View file

@ -0,0 +1,34 @@
<?php
/**
* @file
* Installation functions for Content Translation module.
*/
use Drupal\Core\Language\LanguageInterface;
use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
/**
* Implements hook_install().
*/
function content_translation_install() {
// Assign a fairly low weight to ensure our implementation of
// hook_module_implements_alter() is run among the last ones.
module_set_weight('content_translation', 10);
}
/**
* Implements hook_enable().
*/
function content_translation_enable() {
// Translation works when at least two languages are added.
if (count(\Drupal::languageManager()->getLanguages()) < 2) {
$t_args = array('!language_url' => \Drupal::url('entity.configurable_language.collection'));
$message = t('Be sure to <a href="!language_url">add at least two languages</a> to translate content.', $t_args);
drupal_set_message($message, 'warning');
}
// Point the user to the content translation settings.
$t_args = array('!settings_url' => \Drupal::url('language.content_settings_page'));
$message = t('<a href="!settings_url">Enable translation</a> for <em>content types</em>, <em>taxonomy vocabularies</em>, <em>accounts</em>, or any other element you wish to translate.', $t_args);
drupal_set_message($message, 'warning');
}

View file

@ -0,0 +1,11 @@
drupal.content_translation.admin:
version: VERSION
js:
content_translation.admin.js: {}
css:
theme:
css/content_translation.admin.css: {}
dependencies:
- core/jquery
- core/drupal
- core/jquery.once

View file

@ -0,0 +1,3 @@
content_translation.contextual_links:
deriver: 'Drupal\content_translation\Plugin\Derivative\ContentTranslationContextualLinks'
weight: 2

View file

@ -0,0 +1,3 @@
content_translation.local_tasks:
deriver: 'Drupal\content_translation\Plugin\Derivative\ContentTranslationLocalTasks'
weight: 100

View file

@ -0,0 +1,574 @@
<?php
/**
* @file
* Allows entities to be translated into different languages.
*/
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\ContentEntityFormInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function content_translation_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.content_translation':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Content Translation module allows you to translate content, comments, custom blocks, taxonomy terms, users and other <a href="!field_help" title="Field module help, with background on content entities">content entities</a>. Together with the modules <a href="!language">Language</a>, <a href="!config-trans">Configuration Translation</a>, and <a href="!locale">Interface Translation</a>, it allows you to build multilingual websites. For more information, see <a href="!translation-entity">the online documentation for the Content Translation module</a>.', array('!locale' => (\Drupal::moduleHandler()->moduleExists('locale')) ? \Drupal::url('help.page', array('name' => 'locale')) : '#', '!config-trans' => (\Drupal::moduleHandler()->moduleExists('config_translation')) ? \Drupal::url('help.page', array('name' => 'config_translation')) : '#', '!language' => \Drupal::url('help.page', array('name' => 'language')), '!translation-entity' => 'https://www.drupal.org/documentation/modules/translation', '!field_help' => \Drupal::url('help.page', array('name' => 'field')))) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Enabling translation') . '</dt>';
$output .= '<dd>' . t('In order to translate content, the website must have at least two <a href="!url">languages</a>. When that is the case, you can enable translation for the desired content entities on the <a href="!translation-entity">Content language</a> page. When enabling translation you can choose the default language for content and decide whether to show the language selection field on the content editing forms.', array('!url' => \Drupal::url('entity.configurable_language.collection'), '!translation-entity' => \Drupal::url('language.content_settings_page'), '!language-help' => \Drupal::url('help.page', array('name' => 'language')))) . '</dd>';
$output .= '<dt>' . t('Enabling field translation') . '</dt>';
$output .= '<dd>' . t('You can define which fields of a content entity can be translated. For example, you might want to translate the title and body field while leaving the image field untranslated. If you exclude a field from being translated, it will still show up in the content editing form, but any changes made to that field will be applied to <em>all</em> translations of that content.') . '</dd>';
$output .= '<dt>' . t('Translating content') . '</dt>';
$output .= '<dd>' . t('If translation is enabled you can translate a content entity via the Translate tab (or Translate link). The Translations page of a content entity gives an overview of the translation status for the current content and lets you add, edit, and delete its translations. This process is similar for every translatable content entity on your site.') . '</dd>';
$output .= '<dt>' . t('Changing the source language for a translation') . '</dt>';
$output .= '<dd>' . t('When you add a new translation, the original text you are translating is displayed in the edit form as the <em>source</em>. If at least one translation of the original content already exists when you add a new translation, you can choose either the original content (default) or one of the other translations as the source, using the select list in the Source language section. After saving the translation, the chosen source language is then listed on the Translate tab of the content.') . '</dd>';
$output .= '<dt>' . t('Setting status of translations') . '</dt>';
$output .= '<dd>' . t('If you edit a translation in one language you may want to set the status of the other translations as <em>out-of-date</em>. You can set this status by selecting the <em>Flag other translations as outdated</em> checkbox in the Translation section of the content editing form. The status will be visible on the Translations page.') . '</dd>';
$output .= '</dl>';
return $output;
case 'language.content_settings_page':
$output = '';
if (!\Drupal::languageManager()->isMultilingual()) {
$output .= '<br/>' . t('Before you can translate content, there must be at least two languages added on the <a href="!url">languages administration</a> page.', array('!url' => \Drupal::url('entity.configurable_language.collection')));
}
return $output;
}
}
/**
* Implements hook_module_implements_alter().
*/
function content_translation_module_implements_alter(&$implementations, $hook) {
switch ($hook) {
// Move some of our hook implementations to the end of the list.
case 'entity_type_alter':
$group = $implementations['content_translation'];
unset($implementations['content_translation']);
$implementations['content_translation'] = $group;
break;
}
}
/**
* Implements hook_language_type_info_alter().
*/
function content_translation_language_types_info_alter(array &$language_types) {
// Make content language negotiation configurable by removing the 'locked'
// flag.
$language_types[LanguageInterface::TYPE_CONTENT]['locked'] = FALSE;
unset($language_types[LanguageInterface::TYPE_CONTENT]['fixed']);
}
/**
* Implements hook_entity_type_alter().
*
* The content translation UI relies on the entity info to provide its features.
* See the documentation of hook_entity_type_build() in the Entity API
* documentation for more details on all the entity info keys that may be
* defined.
*
* To make Content Translation automatically support an entity type some keys
* may need to be defined, but none of them is required unless the entity path
* is different from the usual /ENTITY_TYPE/{ENTITY_TYPE} pattern (for instance
* "/taxonomy/term/{taxonomy_term}"). Here are a list of those optional keys:
* - canonical: This key (in the 'links' entity info property) must be defined
* if the entity path is different from /ENTITY_TYPE/{ENTITY_TYPE}
* - translation: This key (in the 'handlers' entity annotation property)
* specifies the translation handler for the entity type. If an entity type is
* translatable and no translation handler is defined,
* \Drupal\content_translation\ContentTranslationHandler will be assumed.
* Every translation handler must implement
* \Drupal\content_translation\ContentTranslationHandlerInterface.
* - content_translation_ui_skip: By default, entity types that do not have a
* canonical link template cannot be enabled for translation. Setting this key
* to TRUE overrides that. When that key is set, the Content Translation
* module will not provide any UI for translating the entity type, and the
* entity type should implement its own UI. This is useful for (e.g.) entity
* types that are embedded into others for editing (which would not need a
* canonical link, but could still support translation).
* - content_translation_metadata: To implement its business logic the content
* translation UI relies on various metadata items describing the translation
* state. The default implementation is provided by
* \Drupal\content_translation\ContentTranslationMetadataWrapper, which is
* relying on one field for each metadata item (field definitions are provided
* by the translation handler). Entity types needing to customize this
* behavior can specify an alternative class through the
* 'content_translation_metadata' key in the entity type definition. Every
* content translation metadata wrapper needs to implement
* \Drupal\content_translation\ContentTranslationMetadataWrapperInterface.
*
* If the entity paths match the default pattern above and there is no need for
* an entity-specific translation handler, Content Translation will provide
* built-in support for the entity. However enabling translation for each
* translatable bundle will be required.
*
* @see \Drupal\Core\Entity\Annotation\EntityType
*/
function content_translation_entity_type_alter(array &$entity_types) {
// Provide defaults for translation info.
/** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
foreach ($entity_types as $entity_type) {
if ($entity_type->isTranslatable()) {
if (!$entity_type->hasHandlerClass('translation')) {
$entity_type->setHandlerClass('translation', 'Drupal\content_translation\ContentTranslationHandler');
}
if (!$entity_type->get('content_translation_metadata')) {
$entity_type->set('content_translation_metadata', 'Drupal\content_translation\ContentTranslationMetadataWrapper');
}
if (!$entity_type->getFormClass('content_translation_deletion')) {
$entity_type->setFormClass('content_translation_deletion', '\Drupal\content_translation\Form\ContentTranslationDeleteForm');
}
$translation = $entity_type->get('translation');
if (!$translation || !isset($translation['content_translation'])) {
$translation['content_translation'] = array();
}
if ($entity_type->hasLinkTemplate('canonical')) {
// Provide default route names for the translation paths.
if (!$entity_type->hasLinkTemplate('drupal:content-translation-overview')) {
$entity_type->setLinkTemplate('drupal:content-translation-overview', $entity_type->getLinkTemplate('canonical') . '/translations');
}
// @todo Remove this as soon as menu access checks rely on the
// controller. See https://www.drupal.org/node/2155787.
$translation['content_translation'] += array(
'access_callback' => 'content_translation_translate_access',
);
}
$entity_type->set('translation', $translation);
}
}
}
/**
* Implements hook_entity_bundle_info_alter().
*/
function content_translation_entity_bundle_info_alter(&$bundles) {
foreach ($bundles as $entity_type => &$info) {
foreach ($info as $bundle => &$bundle_info) {
$bundle_info['translatable'] = \Drupal::service('content_translation.manager')->isEnabled($entity_type, $bundle);
}
}
}
/**
* Implements hook_entity_base_field_info().
*/
function content_translation_entity_base_field_info(EntityTypeInterface $entity_type) {
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
$manager = \Drupal::service('content_translation.manager');
$entity_type_id = $entity_type->id();
if ($manager->isSupported($entity_type_id)) {
$definitions = $manager->getTranslationHandler($entity_type_id)->getFieldDefinitions();
$installed_storage_definitions = \Drupal::entityManager()->getLastInstalledFieldStorageDefinitions($entity_type_id);
// We return metadata storage fields whenever content translation is enabled
// or it was enabled before, so that we keep translation metadata around
// when translation is disabled.
// @todo Re-evaluate this approach and consider removing field storage
// definitions and the related field data if the entity type has no bundle
// enabled for translation, once base field purging is supported.
// See https://www.drupal.org/node/2282119.
if ($manager->isEnabled($entity_type_id) || array_intersect_key($definitions, $installed_storage_definitions)) {
return $definitions;
}
}
}
/**
* Implements hook_field_info_alter().
*
* Content translation extends the @FieldType annotation with following key:
* - column_groups: contains information about the field type properties
* which columns should be synchronized across different translations and
* which are translatable. This is useful for instance to translate the
* "alt" and "title" textual elements of an image field, while keeping the
* same image on every translation.
*
* @see Drupal\image\Plugin\Field\FieldType\imageItem.
*/
function content_translation_field_info_alter(&$info) {
foreach ($info as $key => $settings) {
// Supply the column_groups key if it's not there.
if (empty($settings['column_groups'])) {
$info[$key]['column_groups'] = array();
}
}
}
/**
* Implements hook_entity_operation().
*/
function content_translation_entity_operation(EntityInterface $entity) {
$operations = array();
if ($entity->hasLinkTemplate('drupal:content-translation-overview') && content_translation_translate_access($entity)->isAllowed()) {
$operations['translate'] = array(
'title' => t('Translate'),
'url' => $entity->urlInfo('drupal:content-translation-overview'),
'weight' => 50,
);
}
return $operations;
}
/**
* Implements hook_views_data_alter().
*/
function content_translation_views_data_alter(array &$data) {
// Add the content translation entity link definition to Views data for entity
// types having translation enabled.
$entity_types = \Drupal::entityManager()->getDefinitions();
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
$manager = \Drupal::service('content_translation.manager');
foreach ($entity_types as $entity_type_id => $entity_type) {
$base_table = $entity_type->getBaseTable();
if (isset($data[$base_table]) && $entity_type->hasLinkTemplate('drupal:content-translation-overview') && $manager->isEnabled($entity_type_id)) {
$t_arguments = ['@entity_type_label' => $entity_type->getLabel()];
$data[$base_table]['translation_link'] = [
'field' => [
'title' => t('Link to translate @entity_type_label', $t_arguments),
'help' => t('Provide a translation link to the @entity_type_label.', $t_arguments),
'id' => 'content_translation_link',
],
];
}
}
}
/**
* Implements hook_menu_links_discovered_alter().
*/
function content_translation_menu_links_discovered_alter(array &$links) {
// Clarify where translation settings are located.
$links['language.content_settings_page']['title'] = 'Content language and translation';
$links['language.content_settings_page']['description'] = 'Configure language and translation support for content.';
}
/**
* Access callback for the translation overview page.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity whose translation overview should be displayed.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
function content_translation_translate_access(EntityInterface $entity) {
$account = \Drupal::currentUser();
$condition = $entity instanceof ContentEntityInterface && !$entity->getUntranslated()->language()->isLocked() && \Drupal::languageManager()->isMultilingual() && $entity->isTranslatable() &&
($account->hasPermission('create content translations') || $account->hasPermission('update content translations') || $account->hasPermission('delete content translations'));
return AccessResult::allowedIf($condition)->cachePerPermissions()->cacheUntilEntityChanges($entity);
}
/**
* Implements hook_form_alter().
*/
function content_translation_form_alter(array &$form, FormStateInterface $form_state) {
$form_object = $form_state->getFormObject();
if (!($form_object instanceof ContentEntityFormInterface)) {
return;
}
$entity = $form_object->getEntity();
$op = $form_object->getOperation();
// Let the content translation handler alter the content entity edit form.
if ($entity instanceof ContentEntityInterface && $entity->isTranslatable() && count($entity->getTranslationLanguages()) > 1 && ($op == 'edit' || $op == 'default')) {
$controller = \Drupal::entityManager()->getHandler($entity->getEntityTypeId(), 'translation');
$controller->entityFormAlter($form, $form_state, $entity);
// @todo Move the following lines to the code generating the property form
// elements once we have an official #multilingual FAPI key.
$translations = $entity->getTranslationLanguages();
$form_langcode = $form_object->getFormLangcode($form_state);
// Handle fields shared between translations when there is at least one
// translation available or a new one is being created.
if (!$entity->isNew() && (!isset($translations[$form_langcode]) || count($translations) > 1)) {
$langcode_key = $entity->getEntityType()->getKey('langcode');
foreach ($entity->getFieldDefinitions() as $field_name => $definition) {
if (isset($form[$field_name]) && $field_name != $langcode_key) {
$form[$field_name]['#multilingual'] = $definition->isTranslatable();
}
}
}
}
}
/**
* Implements hook_language_fallback_candidates_OPERATION_alter().
*
* Performs language fallback for inaccessible translations.
*/
function content_translation_language_fallback_candidates_entity_view_alter(&$candidates, $context) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $context['data'];
$entity_type_id = $entity->getEntityTypeId();
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
$manager = \Drupal::service('content_translation.manager');
if ($manager->isEnabled($entity_type_id, $entity->bundle())) {
$entity_type = $entity->getEntityType();
$permission = $entity_type->getPermissionGranularity() == 'bundle' ? $permission = "translate {$entity->bundle()} $entity_type_id" : "translate $entity_type_id";
$current_user = \Drupal::currentuser();
if (!$current_user->hasPermission('translate any entity') && !$current_user->hasPermission($permission)) {
foreach ($entity->getTranslationLanguages() as $langcode => $language) {
$metadata = $manager->getTranslationMetadata($entity->getTranslation($langcode));
if (!$metadata->isPublished()) {
unset($candidates[$langcode]);
}
}
}
}
}
/**
* Implements hook_entity_extra_field_info().
*/
function content_translation_entity_extra_field_info() {
$extra = array();
foreach (\Drupal::entityManager()->getDefinitions() as $entity_type => $info) {
foreach (entity_get_bundles($entity_type) as $bundle => $bundle_info) {
if (\Drupal::service('content_translation.manager')->isEnabled($entity_type, $bundle)) {
$extra[$entity_type][$bundle]['form']['translation'] = array(
'label' => t('Translation'),
'description' => t('Translation settings'),
'weight' => 10,
);
}
}
}
return $extra;
}
/**
* Implements hook_form_FORM_ID_alter() for 'field_config_edit_form'.
*/
function content_translation_form_field_config_edit_form_alter(array &$form, FormStateInterface $form_state) {
$field = $form_state->getFormObject()->getEntity();
$bundle_is_translatable = \Drupal::service('content_translation.manager')->isEnabled($field->getTargetEntityTypeId(), $field->getTargetBundle());
$form['translatable'] = array(
'#type' => 'checkbox',
'#title' => t('Users may translate this field'),
'#default_value' => $field->isTranslatable(),
'#weight' => -1,
'#disabled' => !$bundle_is_translatable,
'#access' => $field->getFieldStorageDefinition()->isTranslatable(),
);
// Provide helpful pointers for administrators.
if (\Drupal::currentUser()->hasPermission('administer content translation') && !$bundle_is_translatable) {
$toggle_url = \Drupal::url('language.content_settings_page', array(), array(
'query' => \Drupal::destination()->getAsArray(),
));
$form['translatable']['#description'] = t('To configure translation for this field, <a href="@language-settings-url">enable language support</a> for this type.', array(
'@language-settings-url' => $toggle_url,
));
}
if ($field->isTranslatable()) {
module_load_include('inc', 'content_translation', 'content_translation.admin');
$element = content_translation_field_sync_widget($field);
if ($element) {
$form['third_party_settings']['content_translation']['translation_sync'] = $element;
$form['third_party_settings']['content_translation']['translation_sync']['#weight'] = -10;
}
}
}
/**
* Implements hook_entity_presave().
*/
function content_translation_entity_presave(EntityInterface $entity) {
if ($entity instanceof ContentEntityInterface && $entity->isTranslatable() && !$entity->isNew()) {
// If we are creating a new translation we need to use the source language
// as original language, since source values are the only ones available to
// compare against.
if (!isset($entity->original)) {
$entity->original = entity_load_unchanged($entity->entityType(), $entity->id());
}
$langcode = $entity->language()->getId();
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
$manager = \Drupal::service('content_translation.manager');
$source_langcode = !$entity->original->hasTranslation($langcode) ? $manager->getTranslationMetadata($entity)->getSource() : NULL;
\Drupal::service('content_translation.synchronizer')->synchronizeFields($entity, $langcode, $source_langcode);
}
}
/**
* Implements hook_element_info_alter().
*/
function content_translation_element_info_alter(&$type) {
if (isset($type['language_configuration'])) {
$type['language_configuration']['#process'][] = 'content_translation_language_configuration_element_process';
}
}
/**
* Returns a widget to enable content translation per entity bundle.
*
* Backward compatibility layer to support entities not using the language
* configuration form element.
*
* @todo Remove once all core entities have language configuration.
*
* @param string $entity_type
* The type of the entity being configured for translation.
* @param string $bundle
* The bundle of the entity being configured for translation.
* @param array $form
* The configuration form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
function content_translation_enable_widget($entity_type, $bundle, array &$form, FormStateInterface $form_state) {
$key = $form_state->get(['content_translation', 'key']);
$context = $form_state->get(['language', $key]) ?: [];
$context += ['entity_type' => $entity_type, 'bundle' => $bundle];
$form_state->set(['language', $key], $context);
$element = content_translation_language_configuration_element_process(array('#name' => $key), $form_state, $form);
unset($element['content_translation']['#element_validate']);
return $element;
}
/**
* Process callback: Expands the language_configuration form element.
*
* @param array $element
* Form API element.
*
* @return
* Processed language configuration element.
*/
function content_translation_language_configuration_element_process(array $element, FormStateInterface $form_state, array &$form) {
if (empty($element['#content_translation_skip_alter']) && \Drupal::currentUser()->hasPermission('administer content translation')) {
$key = $element['#name'];
$form_state->set(['content_translation', 'key'], $key);
$context = $form_state->get(['language', $key]);
$element['content_translation'] = array(
'#type' => 'checkbox',
'#title' => t('Enable translation'),
// For new bundle, we don't know the bundle name yet,
// default to no translatability.
'#default_value' => $context['bundle'] ? \Drupal::service('content_translation.manager')->isEnabled($context['entity_type'], $context['bundle']) : FALSE,
'#element_validate' => array('content_translation_language_configuration_element_validate'),
'#prefix' => '<label>' . t('Translation') . '</label>',
);
$submit_name = isset($form['actions']['save_continue']) ? 'save_continue' : 'submit';
$form['actions'][$submit_name]['#submit'][] = 'content_translation_language_configuration_element_submit';
}
return $element;
}
/**
* Form validation handler for element added with content_translation_language_configuration_element_process().
*
* Checks whether translation can be enabled: if language is set to one of the
* special languages and language selector is not hidden, translation cannot be
* enabled.
*
* @see content_translation_language_configuration_element_submit()
*/
function content_translation_language_configuration_element_validate($element, FormStateInterface $form_state, array $form) {
$key = $form_state->get(['content_translation', 'key']);
$values = $form_state->getValue($key);
if (!$values['language_alterable'] && $values['content_translation'] && \Drupal::languageManager()->isLanguageLocked($values['langcode'])) {
foreach (\Drupal::languageManager()->getLanguages(LanguageInterface::STATE_LOCKED) as $language) {
$locked_languages[] = $language->getName();
}
// @todo Set the correct form element name as soon as the element parents
// are correctly set. We should be using NestedArray::getValue() but for
// now we cannot.
$form_state->setErrorByName('', t('"Show language selector" is not compatible with translating content that has default language: %choice. Either do not hide the language selector or pick a specific language.', array('%choice' => $locked_languages[$values['langcode']])));
}
}
/**
* Form submission handler for element added with content_translation_language_configuration_element_process().
*
* Stores the content translation settings.
*
* @see content_translation_language_configuration_element_validate()
*/
function content_translation_language_configuration_element_submit(array $form, FormStateInterface $form_state) {
$key = $form_state->get(['content_translation', 'key']);
$context = $form_state->get(['language', $key]);
$enabled = $form_state->getValue(array($key, 'content_translation'));
if (\Drupal::service('content_translation.manager')->isEnabled($context['entity_type'], $context['bundle']) != $enabled) {
\Drupal::service('content_translation.manager')->setEnabled($context['entity_type'], $context['bundle'], $enabled);
\Drupal::entityManager()->clearCachedDefinitions();
\Drupal::service('router.builder')->setRebuildNeeded();
}
}
/**
* Implements hook_form_FORM_ID_alter() for language_content_settings_form().
*/
function content_translation_form_language_content_settings_form_alter(array &$form, FormStateInterface $form_state) {
module_load_include('inc', 'content_translation', 'content_translation.admin');
_content_translation_form_language_content_settings_form_alter($form, $form_state);
}
/**
* Implements hook_preprocess_HOOK() for theme_language_content_settings_table().
*/
function content_translation_preprocess_language_content_settings_table(&$variables) {
module_load_include('inc', 'content_translation', 'content_translation.admin');
_content_translation_preprocess_language_content_settings_table($variables);
}
/**
* Implements hook_page_attachments().
*/
function content_translation_page_attachments(&$page) {
$route_match = \Drupal::routeMatch();
// If the current route has no parameters, return.
if (!($route = $route_match->getRouteObject()) || !($parameters = $route->getOption('parameters'))) {
return;
}
// Determine if the current route represents an entity.
foreach ($parameters as $name => $options) {
if (!isset($options['type']) || strpos($options['type'], 'entity:') !== 0) {
continue;
}
$entity = $route_match->getParameter($name);
if ($entity instanceof ContentEntityInterface) {
// Current route represents a content entity. Build hreflang links.
foreach ($entity->getTranslationLanguages() as $language) {
$url = $entity->urlInfo()
->setOption('language', $language)
->setAbsolute()
->toString();
$page['#attached']['html_head_link'][] = array(
array(
'rel' => 'alternate',
'hreflang' => $language->getId(),
'href' => $url,
),
TRUE,
);
}
}
// Since entity was found, no need to iterate further.
return;
}
}

View file

@ -0,0 +1,14 @@
administer content translation:
title: 'Administer translation settings'
description: 'Configure translatability of entities and fields.'
create content translations:
title: 'Create translations'
update content translations:
title: 'Edit translations'
delete content translations:
title: 'Delete translations'
translate any entity:
title: 'Translate any entity'
permission_callbacks:
- \Drupal\content_translation\ContentTranslationPermissions::contentPermissions

View file

@ -0,0 +1,32 @@
services:
content_translation.synchronizer:
class: Drupal\content_translation\FieldTranslationSynchronizer
arguments: ['@entity.manager']
content_translation.subscriber:
class: Drupal\content_translation\Routing\ContentTranslationRouteSubscriber
arguments: ['@content_translation.manager']
tags:
- { name: event_subscriber }
content_translation.overview_access:
class: Drupal\content_translation\Access\ContentTranslationOverviewAccess
arguments: ['@entity.manager']
tags:
- { name: access_check, applies_to: _access_content_translation_overview }
content_translation.manage_access:
class: Drupal\content_translation\Access\ContentTranslationManageAccessCheck
arguments: ['@entity.manager', '@language_manager']
tags:
- { name: access_check, applies_to: _access_content_translation_manage }
content_translation.manager:
class: Drupal\content_translation\ContentTranslationManager
arguments: ['@entity.manager', '@content_translation.updates_manager']
content_translation.updates_manager:
class: Drupal\content_translation\ContentTranslationUpdatesManager
arguments: ['@entity.manager', '@entity.definition_update_manager']
tags:
- { name: event_subscriber }

View file

@ -0,0 +1,33 @@
/**
* @file
* Styles for the content language administration page.
*/
.language-content-settings-form .bundle {
width: 24%;
}
.language-content-settings-form .field {
padding-left: 3em; /* LTR */
width: 24%;
}
[dir="rtl"] .language-content-settings-form .field {
padding-right: 3em;
padding-left: 1em;
}
.language-content-settings-form .column {
padding-left: 5em; /* LTR */
}
[dir="rtl"] .language-content-settings-form .column {
padding-right: 5em;
padding-left: 1em;
}
.language-content-settings-form .field label,
.language-content-settings-form .column label {
font-weight: normal;
}
.language-content-settings-form .translatable {
width: 1%;
}
.language-content-settings-form .operations {
width: 75%;
}

View file

@ -0,0 +1,130 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Access\ContentTranslationManageAccessCheck.
*/
namespace Drupal\content_translation\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\Route;
/**
* Access check for entity translation CRUD operation.
*/
class ContentTranslationManageAccessCheck implements AccessInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Constructs a ContentTranslationManageAccessCheck object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $manager
* The entity type manager.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(EntityManagerInterface $manager, LanguageManagerInterface $language_manager) {
$this->entityManager = $manager;
$this->languageManager = $language_manager;
}
/**
* Checks translation access for the entity and operation on the given route.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The parametrized route.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
* @param string $source
* (optional) For a create operation, the language code of the source.
* @param string $target
* (optional) For a create operation, the language code of the translation.
* @param string $language
* (optional) For an update or delete operation, the language code of the
* translation being updated or deleted.
* @param string $entity_type_id
* (optional) The entity type ID.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account, $source = NULL, $target = NULL, $language = NULL, $entity_type_id = NULL) {
/* @var \Drupal\Core\Entity\ContentEntityInterface $entity */
if ($entity = $route_match->getParameter($entity_type_id)) {
$operation = $route->getRequirement('_access_content_translation_manage');
$language = $this->languageManager->getLanguage($language) ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT);
$entity_type = $this->entityManager->getDefinition($entity_type_id);
if (in_array($operation, ['update', 'delete'])) {
// Translation operations cannot be performed on the default
// translation.
if ($language->getId() == $entity->getUntranslated()->language()->getId()) {
return AccessResult::forbidden()->cacheUntilEntityChanges($entity);
}
// Editors have no access to the translation operations, as entity
// access already grants them an equal or greater access level.
$templates = ['update' => 'edit-form', 'delete' => 'delete-form'];
if ($entity->access($operation) && $entity_type->hasLinkTemplate($templates[$operation])) {
return AccessResult::forbidden()->cachePerPermissions();
}
}
if ($account->hasPermission('translate any entity')) {
return AccessResult::allowed()->cachePerPermissions();
}
/* @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */
$handler = $this->entityManager->getHandler($entity->getEntityTypeId(), 'translation');
// Load translation.
$translations = $entity->getTranslationLanguages();
$languages = $this->languageManager->getLanguages();
switch ($operation) {
case 'create':
$source_language = $this->languageManager->getLanguage($source) ?: $entity->language();
$target_language = $this->languageManager->getLanguage($target) ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT);
$is_new_translation = ($source_language->getId() != $target_language->getId()
&& isset($languages[$source_language->getId()])
&& isset($languages[$target_language->getId()])
&& !isset($translations[$target_language->getId()]));
return AccessResult::allowedIf($is_new_translation)->cachePerPermissions()->cacheUntilEntityChanges($entity)
->andIf($handler->getTranslationAccess($entity, $operation));
case 'delete':
case 'update':
$has_translation = isset($languages[$language->getId()])
&& $language->getId() != $entity->getUntranslated()->language()->getId()
&& isset($translations[$language->getId()]);
return AccessResult::allowedIf($has_translation)->cachePerPermissions()->cacheUntilEntityChanges($entity)
->andIf($handler->getTranslationAccess($entity, $operation));
}
}
// No opinion.
return AccessResult::neutral();
}
}

View file

@ -0,0 +1,83 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Access\ContentTranslationOverviewAccess.
*/
namespace Drupal\content_translation\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Access check for entity translation overview.
*/
class ContentTranslationOverviewAccess implements AccessInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a ContentTranslationOverviewAccess object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $manager
* The entity type manager.
*/
public function __construct(EntityManagerInterface $manager) {
$this->entityManager = $manager;
}
/**
* Checks access to the translation overview for the entity and bundle.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The parametrized route.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
* @param string $entity_type_id
* The entity type ID.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(RouteMatchInterface $route_match, AccountInterface $account, $entity_type_id) {
/* @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $route_match->getParameter($entity_type_id);
if ($entity && $entity->isTranslatable()) {
// Get entity base info.
$bundle = $entity->bundle();
// Get entity access callback.
$definition = $this->entityManager->getDefinition($entity_type_id);
$translation = $definition->get('translation');
$access_callback = $translation['content_translation']['access_callback'];
$access = call_user_func($access_callback, $entity);
if ($access->isAllowed()) {
return $access;
}
// Check "translate any entity" permission.
if ($account->hasPermission('translate any entity')) {
return AccessResult::allowed()->cachePerPermissions()->inheritCacheability($access);
}
// Check per entity permission.
$permission = "translate {$entity_type_id}";
if ($definition->getPermissionGranularity() == 'bundle') {
$permission = "translate {$bundle} {$entity_type_id}";
}
return AccessResult::allowedIfHasPermission($account, $permission)->inheritCacheability($access);
}
// No opinion.
return AccessResult::neutral();
}
}

View file

@ -0,0 +1,690 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\ContentTranslationHandler.
*/
namespace Drupal\content_translation;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\Entity\User;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base class for content translation handlers.
*
* @ingroup entity_api
*/
class ContentTranslationHandler implements ContentTranslationHandlerInterface, EntityHandlerInterface {
use DependencySerializationTrait;
/**
* The type of the entity being translated.
*
* @var string
*/
protected $entityTypeId;
/**
* Information about the entity type.
*
* @var \Drupal\Core\Entity\EntityTypeInterface
*/
protected $entityType;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The content translation manager.
*
* @var \Drupal\content_translation\ContentTranslationManagerInterface
*/
protected $manager;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The array of installed field storage definitions for the entity type, keyed
* by field name.
*
* @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
*/
protected $fieldStorageDefinitions;
/**
* Initializes an instance of the content translation controller.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The info array of the given entity type.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\content_translation\ContentTranslationManagerInterface $manager
* The content translation manager service.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(EntityTypeInterface $entity_type, LanguageManagerInterface $language_manager, ContentTranslationManagerInterface $manager, EntityManagerInterface $entity_manager, AccountInterface $current_user) {
$this->entityTypeId = $entity_type->id();
$this->entityType = $entity_type;
$this->languageManager = $language_manager;
$this->manager = $manager;
$this->currentUser = $current_user;
$this->fieldStorageDefinitions = $entity_manager->getLastInstalledFieldStorageDefinitions($this->entityTypeId);
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('language_manager'),
$container->get('content_translation.manager'),
$container->get('entity.manager'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function getFieldDefinitions() {
$definitions = array();
$definitions['content_translation_source'] = BaseFieldDefinition::create('language')
->setLabel(t('Translation source'))
->setDescription(t('The source language from which this translation was created.'))
->setDefaultValue(LanguageInterface::LANGCODE_NOT_SPECIFIED)
->setRevisionable(TRUE)
->setTranslatable(TRUE);
$definitions['content_translation_outdated'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Translation outdated'))
->setDescription(t('A boolean indicating whether this translation needs to be updated.'))
->setDefaultValue(FALSE)
->setRevisionable(TRUE)
->setTranslatable(TRUE);
if (!$this->hasAuthor()) {
$definitions['content_translation_uid'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Translation author'))
->setDescription(t('The author of this translation.'))
->setSetting('target_type', 'user')
->setSetting('handler', 'default')
->setRevisionable(TRUE)
->setDefaultValueCallback(get_class($this) . '::getDefaultOwnerId')
->setTranslatable(TRUE);
}
if (!$this->hasPublishedStatus()) {
$definitions['content_translation_status'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Translation status'))
->setDescription(t('A boolean indicating whether the translation is visible to non-translators.'))
->setDefaultValue(TRUE)
->setRevisionable(TRUE)
->setTranslatable(TRUE);
}
if (!$this->hasCreatedTime()) {
$definitions['content_translation_created'] = BaseFieldDefinition::create('created')
->setLabel(t('Translation created time'))
->setDescription(t('The Unix timestamp when the translation was created.'))
->setRevisionable(TRUE)
->setTranslatable(TRUE);
}
if (!$this->hasChangedTime()) {
$definitions['content_translation_changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Translation changed time'))
->setDescription(t('The Unix timestamp when the translation was most recently saved.'))
->setRevisionable(TRUE)
->setTranslatable(TRUE);
}
return $definitions;
}
/**
* Checks whether the entity type supports author natively.
*
* @return bool
* TRUE if metadata is natively supported, FALSE otherwise.
*/
protected function hasAuthor() {
// Check for field named uid, but only in case the entity implements the
// EntityOwnerInterface. This helps to exclude cases, where the uid is
// defined as field name, but is not meant to be an owner field e.g. the
// User entity.
return $this->entityType->isSubclassOf('\Drupal\user\EntityOwnerInterface') && $this->checkFieldStorageDefinitionTranslatability('uid');
}
/**
* Checks whether the entity type supports published status natively.
*
* @return bool
* TRUE if metadata is natively supported, FALSE otherwise.
*/
protected function hasPublishedStatus() {
return $this->checkFieldStorageDefinitionTranslatability('status');
}
/**
* Checks whether the entity type supports modification time natively.
*
* @return bool
* TRUE if metadata is natively supported, FALSE otherwise.
*/
protected function hasChangedTime() {
return $this->entityType->isSubclassOf('Drupal\Core\Entity\EntityChangedInterface') && $this->checkFieldStorageDefinitionTranslatability('changed');
}
/**
* Checks whether the entity type supports creation time natively.
*
* @return bool
* TRUE if metadata is natively supported, FALSE otherwise.
*/
protected function hasCreatedTime() {
return $this->checkFieldStorageDefinitionTranslatability('created');
}
/**
* Checks the field storage definition for translatability support.
*
* Checks whether the given field is defined in the field storage definitions
* and if its definition specifies it as translatable.
*
* @param string $field_name
* The name of the field.
*
* @return bool
* TRUE if translatable field storage definition exists, FALSE otherwise.
*/
protected function checkFieldStorageDefinitionTranslatability($field_name) {
return array_key_exists($field_name, $this->fieldStorageDefinitions) && $this->fieldStorageDefinitions[$field_name]->isTranslatable();
}
/**
* {@inheritdoc}
*/
public function retranslate(EntityInterface $entity, $langcode = NULL) {
$updated_langcode = !empty($langcode) ? $langcode : $entity->language()->getId();
foreach ($entity->getTranslationLanguages() as $langcode => $language) {
$this->manager->getTranslationMetadata($entity->getTranslation($langcode))
->setOutdated($langcode != $updated_langcode);
}
}
/**
* {@inheritdoc}
*/
public function getTranslationAccess(EntityInterface $entity, $op) {
// @todo Move this logic into a translation access control handler checking also
// the translation language and the given account.
$entity_type = $entity->getEntityType();
$translate_permission = TRUE;
// If no permission granularity is defined this entity type does not need an
// explicit translate permission.
if (!$this->currentUser->hasPermission('translate any entity') && $permission_granularity = $entity_type->getPermissionGranularity()) {
$translate_permission = $this->currentUser->hasPermission($permission_granularity == 'bundle' ? "translate {$entity->bundle()} {$entity->getEntityTypeId()}" : "translate {$entity->getEntityTypeId()}");
}
return AccessResult::allowedIf($translate_permission && $this->currentUser->hasPermission("$op content translations"))->cachePerPermissions();
}
/**
* {@inheritdoc}
*/
public function getSourceLangcode(FormStateInterface $form_state) {
if ($source = $form_state->get(['content_translation', 'source'])) {
return $source->getId();
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function entityFormAlter(array &$form, FormStateInterface $form_state, EntityInterface $entity) {
$form_object = $form_state->getFormObject();
$form_langcode = $form_object->getFormLangcode($form_state);
$entity_langcode = $entity->getUntranslated()->language()->getId();
$source_langcode = $this->getSourceLangcode($form_state);
$new_translation = !empty($source_langcode);
$translations = $entity->getTranslationLanguages();
if ($new_translation) {
// Make sure a new translation does not appear as existing yet.
unset($translations[$form_langcode]);
}
$is_translation = !$form_object->isDefaultFormLangcode($form_state);
$has_translations = count($translations) > 1;
// Adjust page title to specify the current language being edited, if we
// have at least one translation.
$languages = $this->languageManager->getLanguages();
if (isset($languages[$form_langcode]) && ($has_translations || $new_translation)) {
$title = $this->entityFormTitle($entity);
// When editing the original values display just the entity label.
if ($form_langcode != $entity_langcode) {
$t_args = array('%language' => $languages[$form_langcode]->getName(), '%title' => $entity->label(), '!title' => $title);
$title = empty($source_langcode) ? t('!title [%language translation]', $t_args) : t('Create %language translation of %title', $t_args);
}
$form['#title'] = $title;
}
// Display source language selector only if we are creating a new
// translation and there are at least two translations available.
if ($has_translations && $new_translation) {
$form['source_langcode'] = array(
'#type' => 'details',
'#title' => t('Source language: @language', array('@language' => $languages[$source_langcode]->getName())),
'#tree' => TRUE,
'#weight' => -100,
'#multilingual' => TRUE,
'source' => array(
'#title' => t('Select source language'),
'#title_display' => 'invisible',
'#type' => 'select',
'#default_value' => $source_langcode,
'#options' => array(),
),
'submit' => array(
'#type' => 'submit',
'#value' => t('Change'),
'#submit' => array(array($this, 'entityFormSourceChange')),
),
);
foreach ($this->languageManager->getLanguages() as $language) {
if (isset($translations[$language->getId()])) {
$form['source_langcode']['source']['#options'][$language->getId()] = $language->getName();
}
}
}
// Locate the language widget.
$langcode_key = $this->entityType->getKey('langcode');
if (isset($form[$langcode_key])) {
$language_widget = &$form[$langcode_key];
}
// If we are editing the source entity, limit the list of languages so that
// it is not possible to switch to a language for which a translation
// already exists. Note that this will only work if the widget is structured
// like \Drupal\Core\Field\Plugin\Field\FieldWidget\LanguageSelectWidget.
if (isset($language_widget['widget'][0]['value']) && !$is_translation && $has_translations) {
$language_select = &$language_widget['widget'][0]['value'];
if ($language_select['#type'] == 'language_select') {
$options = array();
foreach ($this->languageManager->getLanguages() as $language) {
// Show the current language, and the languages for which no
// translation already exists.
if (empty($translations[$language->getId()]) || $language->getId() == $entity_langcode) {
$options[$language->getId()] = $language->getName();
}
}
$language_select['#options'] = $options;
}
}
if ($is_translation) {
if (isset($language_widget)) {
$language_widget['widget']['#access'] = FALSE;
}
// Replace the delete button with the delete translation one.
if (!$new_translation) {
$weight = 100;
foreach (array('delete', 'submit') as $key) {
if (isset($form['actions'][$key]['weight'])) {
$weight = $form['actions'][$key]['weight'];
break;
}
}
$access = $this->getTranslationAccess($entity, 'delete')->isAllowed() || ($entity->access('delete') && $this->entityType->hasLinkTemplate('delete-form'));
$form['actions']['delete_translation'] = array(
'#type' => 'submit',
'#value' => t('Delete translation'),
'#weight' => $weight,
'#submit' => array(array($this, 'entityFormDeleteTranslation')),
'#access' => $access,
);
}
// Always remove the delete button on translation forms.
unset($form['actions']['delete']);
}
// We need to display the translation tab only when there is at least one
// translation available or a new one is about to be created.
if ($new_translation || $has_translations) {
$form['content_translation'] = array(
'#type' => 'details',
'#title' => t('Translation'),
'#tree' => TRUE,
'#weight' => 10,
'#access' => $this->getTranslationAccess($entity, $source_langcode ? 'create' : 'update')->isAllowed(),
'#multilingual' => TRUE,
);
// A new translation is enabled by default.
$metadata = $this->manager->getTranslationMetadata($entity);
$status = $new_translation || $metadata->isPublished();
// If there is only one published translation we cannot unpublish it,
// since there would be nothing left to display.
$enabled = TRUE;
if ($status) {
$published = 0;
foreach ($entity->getTranslationLanguages() as $langcode => $language) {
$published += $this->manager->getTranslationMetadata($entity->getTranslation($langcode))
->isPublished();
}
$enabled = $published > 1;
}
$description = $enabled ?
t('An unpublished translation will not be visible without translation permissions.') :
t('Only this translation is published. You must publish at least one more translation to unpublish this one.');
$form['content_translation']['status'] = array(
'#type' => 'checkbox',
'#title' => t('This translation is published'),
'#default_value' => $status,
'#description' => $description,
'#disabled' => !$enabled,
);
$translate = !$new_translation && $metadata->isOutdated();
if (!$translate) {
$form['content_translation']['retranslate'] = array(
'#type' => 'checkbox',
'#title' => t('Flag other translations as outdated'),
'#default_value' => FALSE,
'#description' => t('If you made a significant change, which means the other translations should be updated, you can flag all translations of this content as outdated. This will not change any other property of them, like whether they are published or not.'),
);
}
else {
$form['content_translation']['outdated'] = array(
'#type' => 'checkbox',
'#title' => t('This translation needs to be updated'),
'#default_value' => $translate,
'#description' => t('When this option is checked, this translation needs to be updated. Uncheck when the translation is up to date again.'),
);
$form['content_translation']['#open'] = TRUE;
}
// Default to the anonymous user.
$uid = 0;
if ($new_translation) {
$uid = $this->currentUser->id();
}
elseif (($account = $metadata->getAuthor()) && $account->id()) {
$uid = $account->id();
}
$form['content_translation']['uid'] = array(
'#type' => 'entity_autocomplete',
'#title' => t('Authored by'),
'#target_type' => 'user',
'#default_value' => User::load($uid),
// Validation is done by static::entityFormValidate().
'#validate_reference' => FALSE,
'#maxlength' => 60,
'#description' => t('Leave blank for %anonymous.', array('%anonymous' => \Drupal::config('user.settings')->get('anonymous'))),
);
$date = $new_translation ? REQUEST_TIME : $metadata->getCreatedTime();
$form['content_translation']['created'] = array(
'#type' => 'textfield',
'#title' => t('Authored on'),
'#maxlength' => 25,
'#description' => t('Format: %time. The date format is YYYY-MM-DD and %timezone is the time zone offset from UTC. Leave blank to use the time of form submission.', array('%time' => format_date(REQUEST_TIME, 'custom', 'Y-m-d H:i:s O'), '%timezone' => format_date(REQUEST_TIME, 'custom', 'O'))),
'#default_value' => $new_translation || !$date ? '' : format_date($date, 'custom', 'Y-m-d H:i:s O'),
);
if (isset($language_widget)) {
$language_widget['#multilingual'] = TRUE;
}
$form['#process'][] = array($this, 'entityFormSharedElements');
}
// Process the submitted values before they are stored.
$form['#entity_builders'][] = array($this, 'entityFormEntityBuild');
// Handle entity validation.
if (isset($form['actions']['submit'])) {
$form['actions']['submit']['#validate'][] = array($this, 'entityFormValidate');
}
// Handle entity deletion.
if (isset($form['actions']['delete'])) {
$form['actions']['delete']['#submit'][] = array($this, 'entityFormDelete');
}
}
/**
* Process callback: determines which elements get clue in the form.
*
* @see \Drupal\content_translation\ContentTranslationHandler::entityFormAlter()
*/
public function entityFormSharedElements($element, FormStateInterface $form_state, $form) {
static $ignored_types;
// @todo Find a more reliable way to determine if a form element concerns a
// multilingual value.
if (!isset($ignored_types)) {
$ignored_types = array_flip(array('actions', 'value', 'hidden', 'vertical_tabs', 'token', 'details'));
}
foreach (Element::children($element) as $key) {
if (!isset($element[$key]['#type'])) {
$this->entityFormSharedElements($element[$key], $form_state, $form);
}
else {
// Ignore non-widget form elements.
if (isset($ignored_types[$element[$key]['#type']])) {
continue;
}
// Elements are considered to be non multilingual by default.
if (empty($element[$key]['#multilingual'])) {
// If we are displaying a multilingual entity form we need to provide
// translatability clues, otherwise the shared form elements should be
// hidden.
if (!$form_state->get(['content_translation', 'translation_form'])) {
$this->addTranslatabilityClue($element[$key]);
}
else {
$element[$key]['#access'] = FALSE;
}
}
}
}
return $element;
}
/**
* Adds a clue about the form element translatability.
*
* If the given element does not have a #title attribute, the function is
* recursively applied to child elements.
*
* @param array $element
* A form element array.
*/
protected function addTranslatabilityClue(&$element) {
static $suffix, $fapi_title_elements;
// Elements which can have a #title attribute according to FAPI Reference.
if (!isset($suffix)) {
$suffix = ' <span class="translation-entity-all-languages">(' . t('all languages') . ')</span>';
$fapi_title_elements = array_flip(array('checkbox', 'checkboxes', 'date', 'details', 'fieldset', 'file', 'item', 'password', 'password_confirm', 'radio', 'radios', 'select', 'text_format', 'textarea', 'textfield', 'weight'));
}
// Update #title attribute for all elements that are allowed to have a
// #title attribute according to the Form API Reference. The reason for this
// check is because some elements have a #title attribute even though it is
// not rendered, e.g. field containers.
if (isset($element['#type']) && isset($fapi_title_elements[$element['#type']]) && isset($element['#title'])) {
$element['#title'] .= $suffix;
}
// If the current element does not have a (valid) title, try child elements.
elseif ($children = Element::children($element)) {
foreach ($children as $delta) {
$this->addTranslatabilityClue($element[$delta], $suffix);
}
}
// If there are no children, fall back to the current #title attribute if it
// exists.
elseif (isset($element['#title'])) {
$element['#title'] .= $suffix;
}
}
/**
* Entity builder method.
*
* @param string $entity_type
* The type of the entity.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity whose form is being built.
*
* @see \Drupal\content_translation\ContentTranslationHandler::entityFormAlter()
*/
public function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, FormStateInterface $form_state) {
$form_object = $form_state->getFormObject();
$form_langcode = $form_object->getFormLangcode($form_state);
$values = &$form_state->getValue('content_translation', array());
$metadata = $this->manager->getTranslationMetadata($entity);
$metadata->setAuthor(!empty($values['uid']) ? User::load($values['uid']) : User::load(0));
$metadata->setPublished(!empty($values['status']));
$metadata->setCreatedTime(!empty($values['created']) ? strtotime($values['created']) : REQUEST_TIME);
$metadata->setChangedTime(REQUEST_TIME);
$source_langcode = $this->getSourceLangcode($form_state);
if ($source_langcode) {
$metadata->setSource($source_langcode);
}
$metadata->setOutdated(!empty($values['outdated']));
if (!empty($values['retranslate'])) {
$this->retranslate($entity, $form_langcode);
}
}
/**
* Form validation handler for ContentTranslationHandler::entityFormAlter().
*
* Validates the submitted content translation metadata.
*/
function entityFormValidate($form, FormStateInterface $form_state) {
if (!$form_state->isValueEmpty('content_translation')) {
$translation = $form_state->getValue('content_translation');
// Validate the "authored by" field.
if (!empty($translation['uid']) && !($account = User::load($translation['uid']))) {
$form_state->setErrorByName('content_translation][uid', t('The translation authoring username %name does not exist.', array('%name' => $account->getUsername())));
}
// Validate the "authored on" field.
if (!empty($translation['created']) && strtotime($translation['created']) === FALSE) {
$form_state->setErrorByName('content_translation][created', t('You have to specify a valid translation authoring date.'));
}
}
}
/**
* Form submission handler for ContentTranslationHandler::entityFormAlter().
*
* Takes care of the source language change.
*/
public function entityFormSourceChange($form, FormStateInterface $form_state) {
$form_object = $form_state->getFormObject();
$entity = $form_object->getEntity();
$source = $form_state->getValue(array('source_langcode', 'source'));
$entity_type_id = $entity->getEntityTypeId();
$form_state->setRedirect('content_translation.translation_add_' . $entity_type_id, array(
$entity_type_id => $entity->id(),
'source' => $source,
'target' => $form_object->getFormLangcode($form_state),
));
$languages = $this->languageManager->getLanguages();
drupal_set_message(t('Source language set to: %language', array('%language' => $languages[$source]->getName())));
}
/**
* Form submission handler for ContentTranslationHandler::entityFormAlter().
*
* Takes care of entity deletion.
*/
function entityFormDelete($form, FormStateInterface $form_state) {
$form_object = $form_state->getFormObject()->getEntity();
$entity = $form_object->getEntity();
if (count($entity->getTranslationLanguages()) > 1) {
drupal_set_message(t('This will delete all the translations of %label.', array('%label' => $entity->label())), 'warning');
}
}
/**
* Form submission handler for ContentTranslationHandler::entityFormAlter().
*
* Takes care of content translation deletion.
*/
function entityFormDeleteTranslation($form, FormStateInterface $form_state) {
/** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
$form_object = $form_state->getFormObject();
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $form_object->getEntity();
$entity_type_id = $entity->getEntityTypeId();
if ($entity->access('delete') && $this->entityType->hasLinkTemplate('delete-form')) {
$form_state->setRedirectUrl($entity->urlInfo('delete-form'));
}
else {
$form_state->setRedirect('content_translation.translation_delete_' . $entity_type_id, [
$entity_type_id => $entity->id(),
'language' => $form_object->getFormLangcode($form_state),
]);
}
}
/**
* Returns the title to be used for the entity form page.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity whose form is being altered.
*/
protected function entityFormTitle(EntityInterface $entity) {
return $entity->label();
}
/**
* Default value callback for the owner base field definition.
*
* @return int
* The user ID.
*/
public static function getDefaultOwnerId() {
return \Drupal::currentUser()->id();
}
}

View file

@ -0,0 +1,79 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\ContentTranslationHandlerInterface.
*/
namespace Drupal\content_translation;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Interface for providing content translation.
*
* Defines a set of methods to allow any entity to be processed by the entity
* translation UI.
*/
interface ContentTranslationHandlerInterface {
/**
* Returns a set of field definitions to be used to store metadata items.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface[]
*/
public function getFieldDefinitions();
/**
* Checks if the user can perform the given operation on translations of the
* wrapped entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity whose translation has to be accessed.
* @param $op
* The operation to be performed on the translation. Possible values are:
* - "create"
* - "update"
* - "delete"
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function getTranslationAccess(EntityInterface $entity, $op);
/**
* Retrieves the source language for the translation being created.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return string
* The source language code.
*/
public function getSourceLangcode(FormStateInterface $form_state);
/**
* Marks translations as outdated.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity being translated.
* @param string $langcode
* (optional) The language code of the updated language: all the other
* translations will be marked as outdated. Defaults to the entity language.
*/
public function retranslate(EntityInterface $entity, $langcode = NULL);
/**
* Performs the needed alterations to the entity form.
*
* @param array $form
* The entity form to be altered to provide the translation workflow.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity being created or edited.
*/
public function entityFormAlter(array &$form, FormStateInterface $form_state, EntityInterface $entity);
}

View file

@ -0,0 +1,138 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\ContentTranslationManager.
*/
namespace Drupal\content_translation;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\language\Entity\ContentLanguageSettings;
/**
* Provides common functionality for content translation.
*/
class ContentTranslationManager implements ContentTranslationManagerInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The updates manager.
*
* @var \Drupal\content_translation\ContentTranslationUpdatesManager
*/
protected $updatesManager;
/**
* Constructs a ContentTranslationManageAccessCheck object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $manager
* The entity type manager.
* @param \Drupal\content_translation\ContentTranslationUpdatesManager $updates_manager
* The updates manager.
*/
public function __construct(EntityManagerInterface $manager, ContentTranslationUpdatesManager $updates_manager) {
$this->entityManager = $manager;
$this->updatesManager = $updates_manager;
}
/**
* {@inheritdoc}
*/
function getTranslationHandler($entity_type_id) {
return $this->entityManager->getHandler($entity_type_id, 'translation');
}
/**
* {@inheritdoc}
*/
public function getTranslationMetadata(EntityInterface $translation) {
// We need a new instance of the metadata handler wrapping each translation.
$entity_type = $translation->getEntityType();
$class = $entity_type->get('content_translation_metadata');
return new $class($translation, $this->getTranslationHandler($entity_type->id()));
}
/**
* {@inheritdoc}
*/
public function isSupported($entity_type_id) {
$entity_type = $this->entityManager->getDefinition($entity_type_id);
return $entity_type->isTranslatable() && ($entity_type->hasLinkTemplate('drupal:content-translation-overview') || $entity_type->get('content_translation_ui_skip'));
}
/**
* {@inheritdoc}
*/
public function getSupportedEntityTypes() {
$supported_types = array();
foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($this->isSupported($entity_type_id)) {
$supported_types[$entity_type_id] = $entity_type;
}
}
return $supported_types;
}
/**
* {@inheritdoc}
*/
public function setEnabled($entity_type_id, $bundle, $value) {
$config = $this->loadContentLanguageSettings($entity_type_id, $bundle);
$config->setThirdPartySetting('content_translation', 'enabled', $value)->save();
$entity_type = $this->entityManager->getDefinition($entity_type_id);
$this->updatesManager->updateDefinitions(array($entity_type_id => $entity_type));
}
/**
* {@inheritdoc}
*/
public function isEnabled($entity_type_id, $bundle = NULL) {
$enabled = FALSE;
if ($this->isSupported($entity_type_id)) {
$bundles = !empty($bundle) ? array($bundle) : array_keys($this->entityManager->getBundleInfo($entity_type_id));
foreach ($bundles as $bundle) {
$config = $this->loadContentLanguageSettings($entity_type_id, $bundle);
if ($config->getThirdPartySetting('content_translation', 'enabled', FALSE)) {
$enabled = TRUE;
break;
}
}
}
return $enabled;
}
/**
* Loads a content language config entity based on the entity type and bundle.
*
* @param string $entity_type_id
* ID of the entity type.
* @param string $bundle
* Bundle name.
*
* @return \Drupal\language\Entity\ContentLanguageSettings
* The content language config entity if one exists. Otherwise, returns
* default values.
*/
protected function loadContentLanguageSettings($entity_type_id, $bundle) {
if ($entity_type_id == NULL || $bundle == NULL) {
return NULL;
}
$config = $this->entityManager->getStorage('language_content_settings')->load($entity_type_id . '.' . $bundle);
if ($config == NULL) {
$config = $this->entityManager->getStorage('language_content_settings')->create(['target_entity_type_id' => $entity_type_id, 'target_bundle' => $bundle]);
}
return $config;
}
}

View file

@ -0,0 +1,87 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\ContentTranslationManagerInterface.
*/
namespace Drupal\content_translation;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
/**
* Provides an interface for common functionality for content translation.
*/
interface ContentTranslationManagerInterface {
/**
* Gets the entity types that support content translation.
*
* @return \Drupal\Core\Entity\EntityTypeInterface[]
* An array of entity types that support content translation.
*/
public function getSupportedEntityTypes();
/**
* Checks whether an entity type supports translation.
*
* @param string $entity_type_id
* The entity type.
*
* @return bool
* TRUE if an entity type is supported, FALSE otherwise.
*/
public function isSupported($entity_type_id);
/**
* Returns an instance of the Content translation handler.
*
* @param string $entity_type_id
* The type of the entity being translated.
*
* @return \Drupal\content_translation\ContentTranslationHandlerInterface
* An instance of the content translation handler.
*/
public function getTranslationHandler($entity_type_id);
/**
* Returns an instance of the Content translation metadata.
*
* @param \Drupal\Core\Entity\EntityInterface $translation
* The entity translation whose metadata needs to be retrieved.
*
* @return \Drupal\content_translation\ContentTranslationMetadataWrapperInterface
* An instance of the content translation metadata.
*/
public function getTranslationMetadata(EntityInterface $translation);
/**
* Sets the value for translatability of the given entity type bundle.
*
* @param string $entity_type_id
* The entity type.
* @param string $bundle
* The bundle of the entity.
* @param bool $value
* The boolean value we need to save.
*/
public function setEnabled($entity_type_id, $bundle, $value);
/**
* Determines whether the given entity type is translatable.
*
* @param string $entity_type_id
* The type of the entity.
* @param string $bundle
* (optional) The bundle of the entity. If no bundle is provided, all the
* available bundles are checked.
*
* @returns bool
* TRUE if the specified bundle is translatable. If no bundle is provided
* returns TRUE if at least one of the entity bundles is translatable.
*
*/
public function isEnabled($entity_type_id, $bundle = NULL);
}

View file

@ -0,0 +1,154 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\ContentTranslationMetadataWrapper.
*/
namespace Drupal\content_translation;
use Drupal\Core\Entity\EntityInterface;
use Drupal\user\UserInterface;
/**
* Base class for content translation metadata wrappers.
*/
class ContentTranslationMetadataWrapper implements ContentTranslationMetadataWrapperInterface {
/**
* The wrapped entity translation.
*
* @var \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\FieldableEntityInterface|\Drupal\Core\TypedData\TranslatableInterface
*/
protected $translation;
/**
* The content translation handler.
*
* @var \Drupal\content_translation\ContentTranslationHandlerInterface
*/
protected $handler;
/**
* Initializes an instance of the content translation metadata handler.
*
* @param EntityInterface $translation
* The entity translation to be wrapped.
* @param ContentTranslationHandlerInterface $handler
* The content translation handler.
*/
public function __construct(EntityInterface $translation, ContentTranslationHandlerInterface $handler) {
$this->translation = $translation;
$this->handler = $handler;
}
/**
* {@inheritdoc}
*/
public function getSource() {
return $this->translation->get('content_translation_source')->value;
}
/**
* {@inheritdoc}
*/
public function setSource($source) {
$this->translation->set('content_translation_source', $source);
return $this;
}
/**
* {@inheritdoc}
*/
public function isOutdated() {
return (bool) $this->translation->get('content_translation_outdated')->value;
}
/**
* {@inheritdoc}
*/
public function setOutdated($outdated) {
$this->translation->set('content_translation_outdated', $outdated);
return $this;
}
/**
* {@inheritdoc}
*/
public function getAuthor() {
return $this->translation->hasField('content_translation_uid') ? $this->translation->get('content_translation_uid')->entity : $this->translation->getOwner();
}
/**
* {@inheritdoc}
*/
public function setAuthor(UserInterface $account) {
$field_name = $this->translation->hasField('content_translation_uid') ? 'content_translation_uid' : 'uid';
$this->setFieldOnlyIfTranslatable($field_name, $account->id());
return $this;
}
/**
* {@inheritdoc}
*/
public function isPublished() {
$field_name = $this->translation->hasField('content_translation_status') ? 'content_translation_status' : 'status';
return (bool) $this->translation->get($field_name)->value;
}
/**
* {@inheritdoc}
*/
public function setPublished($published) {
$field_name = $this->translation->hasField('content_translation_status') ? 'content_translation_status' : 'status';
$this->setFieldOnlyIfTranslatable($field_name, $published);
return $this;
}
/**
* {@inheritdoc}
*/
public function getCreatedTime() {
$field_name = $this->translation->hasField('content_translation_created') ? 'content_translation_created' : 'created';
return $this->translation->get($field_name)->value;
}
/**
* {@inheritdoc}
*/
public function setCreatedTime($timestamp) {
$field_name = $this->translation->hasField('content_translation_created') ? 'content_translation_created' : 'created';
$this->setFieldOnlyIfTranslatable($field_name, $timestamp);
return $this;
}
/**
* {@inheritdoc}
*/
public function getChangedTime() {
return $this->translation->hasField('content_translation_changed') ? $this->translation->get('content_translation_changed')->value : $this->translation->getChangedTime();
}
/**
* {@inheritdoc}
*/
public function setChangedTime($timestamp) {
$field_name = $this->translation->hasField('content_translation_changed') ? 'content_translation_changed' : 'changed';
$this->setFieldOnlyIfTranslatable($field_name, $timestamp);
return $this;
}
/**
* Updates a field value, only if the field is translatable.
*
* @param string $field_name
* The name of the field.
* @param mixed $value
* The field value to be set.
*/
protected function setFieldOnlyIfTranslatable($field_name, $value) {
if ($this->translation->getFieldDefinition($field_name)->isTranslatable()) {
$this->translation->set($field_name, $value);
}
}
}

View file

@ -0,0 +1,136 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\ContentTranslationMetadataWrapperInterface.
*/
namespace Drupal\content_translation;
use Drupal\user\UserInterface;
/**
* Common interface for content translation metadata wrappers.
*
* This acts as a wrapper for an entity translation object, encapsulating the
* logic needed to retrieve translation metadata.
*/
interface ContentTranslationMetadataWrapperInterface {
/**
* Retrieves the source language for this translation.
*
* @return string
* The source language code.
*/
public function getSource();
/**
* Sets the source language for this translation.
*
* @param string $source
* The source language code.
*
* @return $this
*/
public function setSource($source);
/**
* Returns the translation outdated status.
*
* @return bool
* TRUE if the translation is outdated, FALSE otherwise.
*/
public function isOutdated();
/**
* Sets the translation outdated status.
*
* @param bool $outdated
* TRUE if the translation is outdated, FALSE otherwise.
*
* @return $this
*/
public function setOutdated($outdated);
/**
* Returns the translation author.
*
* @return \Drupal\user\UserInterface
* The user entity for the translation author.
*/
public function getAuthor();
/**
* Sets the translation author.
*
* The metadata field will be updated, only if it's translatable.
*
* @param \Drupal\user\UserInterface $account
* The translation author user entity.
*
* @return $this
*/
public function setAuthor(UserInterface $account);
/**
* Returns the translation published status.
*
* @return bool
* TRUE if the translation is published, FALSE otherwise.
*/
public function isPublished();
/**
* Sets the translation published status.
*
* The metadata field will be updated, only if it's translatable.
*
* @param bool $published
* TRUE if the translation is published, FALSE otherwise.
*
* @return $this
*/
public function setPublished($published);
/**
* Returns the translation creation timestamp.
*
* @return int
* The UNIX timestamp of when the translation was created.
*/
public function getCreatedTime();
/**
* Sets the translation creation timestamp.
*
* The metadata field will be updated, only if it's translatable.
*
* @param int $timestamp
* The UNIX timestamp of when the translation was created.
*
* @return $this
*/
public function setCreatedTime($timestamp);
/**
* Returns the timestamp of the last entity change from current translation.
*
* @return int
* The timestamp of the last entity save operation.
*/
public function getChangedTime();
/**
* Sets the translation modification timestamp.
*
* The metadata field will be updated, only if it's translatable.
*
* @param int $timestamp
* The UNIX timestamp of when the translation was last modified.
*
* @return $this
*/
public function setChangedTime($timestamp);
}

View file

@ -0,0 +1,98 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\ContentTranslationPermissions.
*/
namespace Drupal\content_translation;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides dynamic permissions for the content_translation module.
*/
class ContentTranslationPermissions implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The content translation manager.
*
* @var \Drupal\content_translation\ContentTranslationManagerInterface
*/
protected $contentTranslationManager;
/**
* Constructs a ContentTranslationPermissions instance.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
* The content translation manager.
*/
public function __construct(EntityManagerInterface $entity_manager, ContentTranslationManagerInterface $content_translation_manager) {
$this->entityManager = $entity_manager;
$this->contentTranslationManager = $content_translation_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.manager'),
$container->get('content_translation.manager')
);
}
/**
* Returns an array of content translation permissions.
*
* @return array
*/
public function contentPermissions() {
$permission = [];
// Create a translate permission for each enabled entity type and (optionally)
// bundle.
foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($permission_granularity = $entity_type->getPermissionGranularity()) {
$t_args = ['@entity_label' => $entity_type->getLowercaseLabel()];
switch ($permission_granularity) {
case 'bundle':
foreach ($this->entityManager->getBundleInfo($entity_type_id) as $bundle => $bundle_info) {
if ($this->contentTranslationManager->isEnabled($entity_type_id, $bundle)) {
$t_args['%bundle_label'] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle;
$permission["translate $bundle $entity_type_id"] = [
'title' => $this->t('Translate %bundle_label @entity_label', $t_args),
];
}
}
break;
case 'entity_type':
if ($this->contentTranslationManager->isEnabled($entity_type_id)) {
$permission["translate $entity_type_id"] = [
'title' => $this->t('Translate @entity_label', $t_args),
];
}
break;
}
}
}
return $permission;
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\ContentTranslationUpdatesManager.
*/
namespace Drupal\content_translation;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Provides the logic needed to update field storage definitions when needed.
*/
class ContentTranslationUpdatesManager implements EventSubscriberInterface {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The entity definition update manager.
*
* @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
*/
protected $updateManager;
/**
* Constructs an updates manager instance.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $update_manager
* The entity definition update manager.
*/
public function __construct(EntityManagerInterface $entity_manager, EntityDefinitionUpdateManagerInterface $update_manager) {
$this->entityManager = $entity_manager;
$this->updateManager = $update_manager;
}
/**
* Executes field storage definition updates if needed.
*
* @param array $entity_types
* A list of entity type definitions to be processed.
*/
public function updateDefinitions(array $entity_types) {
// Handle field storage definition creation, if needed.
// @todo Generalize this code in https://www.drupal.org/node/2346013.
// @todo Handle initial values in https://www.drupal.org/node/2346019.
if ($this->updateManager->needsUpdates()) {
foreach ($entity_types as $entity_type_id => $entity_type) {
$storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
$installed_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
foreach (array_diff_key($storage_definitions, $installed_storage_definitions) as $storage_definition) {
/** @var $storage_definition \Drupal\Core\Field\FieldStorageDefinitionInterface */
if ($storage_definition->getProvider() == 'content_translation') {
$this->entityManager->onFieldStorageDefinitionCreate($storage_definition);
}
}
}
}
}
/**
* Listener for the ConfigImporter import event.
*/
public function onConfigImporterImport() {
$entity_types = array_filter($this->entityManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
return $entity_type->isTranslatable();
});
$this->updateDefinitions($entity_types);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[ConfigEvents::IMPORT][] = ['onConfigImporterImport', 60];
return $events;
}
}

View file

@ -0,0 +1,364 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Controller\ContentTranslationController.
*/
namespace Drupal\content_translation\Controller;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base class for entity translation controllers.
*/
class ContentTranslationController extends ControllerBase {
/**
* The content translation manager.
*
* @var \Drupal\content_translation\ContentTranslationManagerInterface
*/
protected $manager;
/**
* Initializes a content translation controller.
*
* @param \Drupal\content_translation\ContentTranslationManagerInterface
* A content translation manager instance.
*/
public function __construct(ContentTranslationManagerInterface $manager) {
$this->manager = $manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('content_translation.manager'));
}
/**
* Populates target values with the source values.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being translated.
* @param \Drupal\Core\Language\LanguageInterface $source
* The language to be used as source.
* @param \Drupal\Core\Language\LanguageInterface $target
* The language to be used as target.
*/
public function prepareTranslation(ContentEntityInterface $entity, LanguageInterface $source, LanguageInterface $target) {
/* @var \Drupal\Core\Entity\ContentEntityInterface $source_translation */
$source_translation = $entity->getTranslation($source->getId());
$target_translation = $entity->addTranslation($target->getId(), $source_translation->toArray());
// Make sure we do not inherit the affected status from the source values.
if ($entity->getEntityType()->isRevisionable()) {
$target_translation->setRevisionTranslationAffected(NULL);
}
/** @var \Drupal\user\UserInterface $user */
$user = $this->entityManager()->getStorage('user')->load($this->currentUser()->id());
$metadata = $this->manager->getTranslationMetadata($target_translation);
// Update the translation author to current user, as well the translation
// creation time.
$metadata->setAuthor($user);
$metadata->setCreatedTime(REQUEST_TIME);
}
/**
* Builds the translations overview page.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
* @param string $entity_type_id
* (optional) The entity type ID.
* @return array Array of page elements to render.
* Array of page elements to render.
*/
public function overview(RouteMatchInterface $route_match, $entity_type_id = NULL) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $route_match->getParameter($entity_type_id);
$account = $this->currentUser();
$handler = $this->entityManager()->getHandler($entity_type_id, 'translation');
$manager = $this->manager;
$entity_type = $entity->getEntityType();
$languages = $this->languageManager()->getLanguages();
$original = $entity->getUntranslated()->language()->getId();
$translations = $entity->getTranslationLanguages();
$field_ui = $this->moduleHandler()->moduleExists('field_ui') && $account->hasPermission('administer ' . $entity_type_id . ' fields');
$rows = array();
$show_source_column = FALSE;
if ($this->languageManager()->isMultilingual()) {
// Determine whether the current entity is translatable.
$translatable = FALSE;
foreach ($this->entityManager->getFieldDefinitions($entity_type_id, $entity->bundle()) as $instance) {
if ($instance->isTranslatable()) {
$translatable = TRUE;
break;
}
}
// Show source-language column if there are non-original source langcodes.
$additional_source_langcodes = array_filter(array_keys($translations), function ($langcode) use ($entity, $original, $manager) {
$source = $manager->getTranslationMetadata($entity->getTranslation($langcode))->getSource();
return $source != $original && $source != LanguageInterface::LANGCODE_NOT_SPECIFIED;
});
$show_source_column = !empty($additional_source_langcodes);
foreach ($languages as $language) {
$language_name = $language->getName();
$langcode = $language->getId();
$add_url = new Url(
'content_translation.translation_add_' . $entity_type_id,
array(
'source' => $original,
'target' => $language->getId(),
$entity_type_id => $entity->id(),
),
array(
'language' => $language,
)
);
$edit_url = new Url(
'content_translation.translation_edit_' . $entity_type_id,
array(
'language' => $language->getId(),
$entity_type_id => $entity->id(),
),
array(
'language' => $language,
)
);
$delete_url = new Url(
'content_translation.translation_delete_' . $entity_type_id,
array(
'language' => $language->getId(),
$entity_type_id => $entity->id(),
),
array(
'language' => $language,
)
);
$operations = array(
'data' => array(
'#type' => 'operations',
'#links' => array(),
),
);
$links = &$operations['data']['#links'];
if (array_key_exists($langcode, $translations)) {
// Existing translation in the translation set: display status.
$translation = $entity->getTranslation($langcode);
$metadata = $manager->getTranslationMetadata($translation);
$source = $metadata->getSource() ?: LanguageInterface::LANGCODE_NOT_SPECIFIED;
$is_original = $langcode == $original;
$label = $entity->getTranslation($langcode)->label();
$link = isset($links->links[$langcode]['url']) ? $links->links[$langcode] : array('url' => $entity->urlInfo());
if (!empty($link['url'])) {
$link['url']->setOption('language', $language);
$row_title = $this->l($label, $link['url']);
}
if (empty($link['url'])) {
$row_title = $is_original ? $label : $this->t('n/a');
}
// If the user is allowed to edit the entity we point the edit link to
// the entity form, otherwise if we are not dealing with the original
// language we point the link to the translation form.
if ($entity->access('update') && $entity_type->hasLinkTemplate('edit-form')) {
$links['edit']['url'] = $entity->urlInfo('edit-form');
$links['edit']['language'] = $language;
}
elseif (!$is_original && $handler->getTranslationAccess($entity, 'update')->isAllowed()) {
$links['edit']['url'] = $edit_url;
}
if (isset($links['edit'])) {
$links['edit']['title'] = $this->t('Edit');
}
$status = array('data' => array(
'#type' => 'inline_template',
'#template' => '<span class="status">{% if status %}{{ "Published"|t }}{% else %}{{ "Not published"|t }}{% endif %}</span>{% if outdated %} <span class="marker">{{ "outdated"|t }}</span>{% endif %}',
'#context' => array(
'status' => $metadata->isPublished(),
'outdated' => $metadata->isOutdated(),
),
));
if ($is_original) {
$language_name = $this->t('<strong>@language_name (Original language)</strong>', array('@language_name' => $language_name));
$source_name = $this->t('n/a');
}
else {
$source_name = isset($languages[$source]) ? $languages[$source]->getName() : $this->t('n/a');
if ($entity->access('delete') && $entity_type->hasLinkTemplate('delete-form')) {
$links['delete'] = array(
'title' => $this->t('Delete'),
'url' => $entity->urlInfo('delete-form'),
'language' => $language,
);
}
elseif ($handler->getTranslationAccess($entity, 'delete')->isAllowed()) {
$links['delete'] = array(
'title' => $this->t('Delete'),
'url' => $delete_url,
);
}
}
}
else {
// No such translation in the set yet: help user to create it.
$row_title = $source_name = $this->t('n/a');
$source = $entity->language()->getId();
if ($source != $langcode && $handler->getTranslationAccess($entity, 'create')->isAllowed()) {
if ($translatable) {
$links['add'] = array(
'title' => $this->t('Add'),
'url' => $add_url,
);
}
elseif ($field_ui) {
$url = new Url('language.content_settings_page');
// Link directly to the fields tab to make it easier to find the
// setting to enable translation on fields.
$links['nofields'] = array(
'title' => $this->t('No translatable fields'),
'url' => $url,
);
}
}
$status = $this->t('Not translated');
}
if ($show_source_column) {
$rows[] = array(
$language_name,
$row_title,
$source_name,
$status,
$operations,
);
}
else {
$rows[] = array($language_name, $row_title, $status, $operations);
}
}
}
if ($show_source_column) {
$header = array(
$this->t('Language'),
$this->t('Translation'),
$this->t('Source language'),
$this->t('Status'),
$this->t('Operations'),
);
}
else {
$header = array(
$this->t('Language'),
$this->t('Translation'),
$this->t('Status'),
$this->t('Operations'),
);
}
$build['#title'] = $this->t('Translations of %label', array('%label' => $entity->label()));
// Add metadata to the build render array to let other modules know about
// which entity this is.
$build['#entity'] = $entity;
$build['content_translation_overview'] = array(
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
);
return $build;
}
/**
* Builds an add translation page.
*
* @param \Drupal\Core\Language\LanguageInterface $source
* The language of the values being translated. Defaults to the entity
* language.
* @param \Drupal\Core\Language\LanguageInterface $target
* The language of the translated values. Defaults to the current content
* language.
* @param \Drupal\Core\Routing\RouteMatchInterface
* The route match object from which to extract the entity type.
* @param string $entity_type_id
* (optional) The entity type ID.
*
* @return array
* A processed form array ready to be rendered.
*/
public function add(LanguageInterface $source, LanguageInterface $target, RouteMatchInterface $route_match, $entity_type_id = NULL) {
$entity = $route_match->getParameter($entity_type_id);
// @todo Exploit the upcoming hook_entity_prepare() when available.
// See https://www.drupal.org/node/1810394.
$this->prepareTranslation($entity, $source, $target);
// @todo Provide a way to figure out the default form operation. Maybe like
// $operation = isset($info['default_operation']) ? $info['default_operation'] : 'default';
// See https://www.drupal.org/node/2006348.
$operation = 'default';
$form_state_additions = array();
$form_state_additions['langcode'] = $target->getId();
$form_state_additions['content_translation']['source'] = $source;
$form_state_additions['content_translation']['target'] = $target;
$form_state_additions['content_translation']['translation_form'] = !$entity->access('update');
return $this->entityFormBuilder()->getForm($entity, $operation, $form_state_additions);
}
/**
* Builds the edit translation page.
*
* @param \Drupal\Core\Language\LanguageInterface $language
* The language of the translated values. Defaults to the current content
* language.
* @param \Drupal\Core\Routing\RouteMatchInterface
* The route match object from which to extract the entity type.
* @param string $entity_type_id
* (optional) The entity type ID.
*
* @return array
* A processed form array ready to be rendered.
*/
public function edit(LanguageInterface $language, RouteMatchInterface $route_match, $entity_type_id = NULL) {
$entity = $route_match->getParameter($entity_type_id);
// @todo Provide a way to figure out the default form operation. Maybe like
// $operation = isset($info['default_operation']) ? $info['default_operation'] : 'default';
// See https://www.drupal.org/node/2006348.
$operation = 'default';
$form_state_additions = array();
$form_state_additions['langcode'] = $language->getId();
$form_state_additions['content_translation']['translation_form'] = TRUE;
return $this->entityFormBuilder()->getForm($entity, $operation, $form_state_additions);
}
}

View file

@ -0,0 +1,220 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\FieldTranslationSynchronizer.
*/
namespace Drupal\content_translation;
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
/**
* Provides field translation synchronization capabilities.
*/
class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterface {
/**
* The entity manager to use to load unchanged entities.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a FieldTranslationSynchronizer object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entityManager
* The entity manager.
*/
public function __construct(EntityManagerInterface $entityManager) {
$this->entityManager = $entityManager;
}
/**
* {@inheritdoc}
*/
public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode, $original_langcode = NULL) {
$translations = $entity->getTranslationLanguages();
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
// If we have no information about what to sync to, if we are creating a new
// entity, if we have no translations for the current entity and we are not
// creating one, then there is nothing to synchronize.
if (empty($sync_langcode) || $entity->isNew() || count($translations) < 2) {
return;
}
// If the entity language is being changed there is nothing to synchronize.
$entity_type = $entity->getEntityTypeId();
$entity_unchanged = isset($entity->original) ? $entity->original : $this->entityManager->getStorage($entity_type)->loadUnchanged($entity->id());
if ($entity->getUntranslated()->language()->getId() != $entity_unchanged->getUntranslated()->language()->getId()) {
return;
}
/** @var \Drupal\Core\Field\FieldItemListInterface $items */
foreach ($entity as $field_name => $items) {
$field_definition = $items->getFieldDefinition();
$field_type_definition = $field_type_manager->getDefinition($field_definition->getType());
$column_groups = $field_type_definition['column_groups'];
// Sync if the field is translatable, not empty, and the synchronization
// setting is enabled.
if ($field_definition instanceof ThirdPartySettingsInterface && $field_definition->isTranslatable() && !$items->isEmpty() && $translation_sync = $field_definition->getThirdPartySetting('content_translation', 'translation_sync')) {
// Retrieve all the untranslatable column groups and merge them into
// single list.
$groups = array_keys(array_diff($translation_sync, array_filter($translation_sync)));
if (!empty($groups)) {
$columns = array();
foreach ($groups as $group) {
$info = $column_groups[$group];
// A missing 'columns' key indicates we have a single-column group.
$columns = array_merge($columns, isset($info['columns']) ? $info['columns'] : array($group));
}
if (!empty($columns)) {
$values = array();
foreach ($translations as $langcode => $language) {
$values[$langcode] = $entity->getTranslation($langcode)->get($field_name)->getValue();
}
// If a translation is being created, the original values should be
// used as the unchanged items. In fact there are no unchanged items
// to check against.
$langcode = $original_langcode ?: $sync_langcode;
$unchanged_items = $entity_unchanged->getTranslation($langcode)->get($field_name)->getValue();
$this->synchronizeItems($values, $unchanged_items, $sync_langcode, array_keys($translations), $columns);
foreach ($translations as $langcode => $language) {
$entity->getTranslation($langcode)->get($field_name)->setValue($values[$langcode]);
}
}
}
}
}
}
/**
* {@inheritdoc}
*/
public function synchronizeItems(array &$values, array $unchanged_items, $sync_langcode, array $translations, array $columns) {
$source_items = $values[$sync_langcode];
// Make sure we can detect any change in the source items.
$change_map = array();
// By picking the maximum size between updated and unchanged items, we make
// sure to process also removed items.
$total = max(array(count($source_items), count($unchanged_items)));
// As a first step we build a map of the deltas corresponding to the column
// values to be synchronized. Recording both the old values and the new
// values will allow us to detect any change in the order of the new items
// for each column.
for ($delta = 0; $delta < $total; $delta++) {
foreach (array('old' => $unchanged_items, 'new' => $source_items) as $key => $items) {
if ($item_id = $this->itemHash($items, $delta, $columns)) {
$change_map[$item_id][$key][] = $delta;
}
}
}
// Backup field values and the change map.
$original_field_values = $values;
$original_change_map = $change_map;
// Reset field values so that no spurious one is stored. Source values must
// be preserved in any case.
$values = array($sync_langcode => $source_items);
// Update field translations.
foreach ($translations as $langcode) {
// We need to synchronize only values different from the source ones.
if ($langcode != $sync_langcode) {
// Reinitialize the change map as it is emptied while processing each
// language.
$change_map = $original_change_map;
// By using the maximum cardinality we ensure to process removed items.
for ($delta = 0; $delta < $total; $delta++) {
// By inspecting the map we built before we can tell whether a value
// has been created or removed. A changed value will be interpreted as
// a new value, in fact it did not exist before.
$created = TRUE;
$removed = TRUE;
$old_delta = NULL;
$new_delta = NULL;
if ($item_id = $this->itemHash($source_items, $delta, $columns)) {
if (!empty($change_map[$item_id]['old'])) {
$old_delta = array_shift($change_map[$item_id]['old']);
}
if (!empty($change_map[$item_id]['new'])) {
$new_delta = array_shift($change_map[$item_id]['new']);
}
$created = $created && !isset($old_delta);
$removed = $removed && !isset($new_delta);
}
// If an item has been removed we do not store its translations.
if ($removed) {
continue;
}
// If a synchronized column has changed or has been created from
// scratch we need to override the full items array for all languages.
elseif ($created) {
$values[$langcode][$delta] = $source_items[$delta];
}
// Otherwise the current item might have been reordered.
elseif (isset($old_delta) && isset($new_delta)) {
// If for any reason the old value is not defined for the current
// language we fall back to the new source value, this way we ensure
// the new values are at least propagated to all the translations.
// If the value has only been reordered we just move the old one in
// the new position.
$item = isset($original_field_values[$langcode][$old_delta]) ? $original_field_values[$langcode][$old_delta] : $source_items[$new_delta];
$values[$langcode][$new_delta] = $item;
}
}
}
}
}
/**
* Computes a hash code for the specified item.
*
* @param array $items
* An array of field items.
* @param integer $delta
* The delta identifying the item to be processed.
* @param array $columns
* An array of column names to be synchronized.
*
* @returns string
* A hash code that can be used to identify the item.
*/
protected function itemHash(array $items, $delta, array $columns) {
$values = array();
if (isset($items[$delta])) {
foreach ($columns as $column) {
if (!empty($items[$delta][$column])) {
$value = $items[$delta][$column];
// String and integer values are by far the most common item values,
// thus we special-case them to improve performance.
$values[] = is_string($value) || is_int($value) ? $value : hash('sha256', serialize($value));
}
else {
// Explicitly track also empty values.
$values[] = '';
}
}
}
return implode('.', $values);
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\FieldTranslationSynchronizerInterface.
*/
namespace Drupal\content_translation;
use Drupal\Core\Entity\ContentEntityInterface;
/**
* Provides field translation synchronization capabilities.
*/
interface FieldTranslationSynchronizerInterface {
/**
* Performs field column synchronization on the given entity.
*
* Field column synchronization takes care of propagating any change in the
* field items order and in the column values themselves to all the available
* translations. This functionality is provided by defining a
* 'translation_sync' key for the 'content_translation' module's portion of
* the field definition's 'third_party_settings', holding an array of
* column names to be synchronized. The synchronized column values are shared
* across translations, while the rest varies per-language. This is useful for
* instance to translate the "alt" and "title" textual elements of an image
* field, while keeping the same image on every translation.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity whose values should be synchronized.
* @param string $sync_langcode
* The language of the translation whose values should be used as source for
* synchronization.
* @param string $original_langcode
* (optional) If a new translation is being created, this should be the
* language code of the original values. Defaults to NULL.
*/
public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode, $original_langcode = NULL);
/**
* Synchronize the items of a single field.
*
* All the column values of the "active" language are compared to the
* unchanged values to detect any addition, removal or change in the items
* order. Subsequently the detected changes are performed on the field items
* in other available languages.
*
* @param array $field_values
* The field values to be synchronized.
* @param array $unchanged_items
* The unchanged items to be used to detect changes.
* @param string $sync_langcode
* The language code of the items to use as source values.
* @param array $translations
* An array of all the available language codes for the given field.
* @param array $columns
* An array of column names to be synchronized.
*/
public function synchronizeItems(array &$field_values, array $unchanged_items, $sync_langcode, array $translations, array $columns);
}

View file

@ -0,0 +1,36 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Form\ContentTranslationDeleteForm.
*/
namespace Drupal\content_translation\Form;
use Drupal\Core\Entity\ContentEntityDeleteForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
/**
* Delete translation form for content_translation module.
*/
class ContentTranslationDeleteForm extends ContentEntityDeleteForm {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'content_translation_delete_confirm';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, LanguageInterface $language = NULL) {
if ($language) {
$form_state->set('langcode', $language->getId());
}
return parent::buildForm($form, $form_state);
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Plugin\Derivative\ContentTranslationContextualLinks.
*/
namespace Drupal\content_translation\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides dynamic contextual links for content translation.
*
* @see \Drupal\content_translation\Plugin\Menu\ContextualLink\ContentTranslationContextualLinks
*/
class ContentTranslationContextualLinks extends DeriverBase implements ContainerDeriverInterface {
/**
* The content translation manager.
*
* @var \Drupal\content_translation\ContentTranslationManagerInterface
*/
protected $contentTranslationManager;
/**
* Constructs a new ContentTranslationContextualLinks.
*
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
* The content translation manager.
*/
public function __construct(ContentTranslationManagerInterface $content_translation_manager) {
$this->contentTranslationManager = $content_translation_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('content_translation.manager')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
// Create contextual links for translatable entity types.
foreach ($this->contentTranslationManager->getSupportedEntityTypes() as $entity_type_id => $entity_type) {
$this->derivatives[$entity_type_id]['title'] = t('Translate');
$this->derivatives[$entity_type_id]['route_name'] = "entity.$entity_type_id.content_translation_overview";
$this->derivatives[$entity_type_id]['group'] = $entity_type_id;
}
return parent::getDerivativeDefinitions($base_plugin_definition);
}
}

View file

@ -0,0 +1,77 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Plugin\Derivative\ContentTranslationLocalTasks.
*/
namespace Drupal\content_translation\Plugin\Derivative;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides dynamic local tasks for content translation.
*/
class ContentTranslationLocalTasks extends DeriverBase implements ContainerDeriverInterface {
/**
* The base plugin ID
*
* @var string
*/
protected $basePluginId;
/**
* The content translation manager.
*
* @var \Drupal\content_translation\ContentTranslationManagerInterface
*/
protected $contentTranslationManager;
/**
* Constructs a new ContentTranslationLocalTasks.
*
* @param string $base_plugin_id
* The base plugin ID.
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
* The content translation manager.
*/
public function __construct($base_plugin_id, ContentTranslationManagerInterface $content_translation_manager) {
$this->basePluginId = $base_plugin_id;
$this->contentTranslationManager = $content_translation_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$base_plugin_id,
$container->get('content_translation.manager')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
// Create tabs for all possible entity types.
foreach ($this->contentTranslationManager->getSupportedEntityTypes() as $entity_type_id => $entity_type) {
// Find the route name for the translation overview.
$translation_route_name = "entity.$entity_type_id.content_translation_overview";
$base_route_name = "entity.$entity_type_id.canonical";
$this->derivatives[$translation_route_name] = array(
'entity_type' => $entity_type_id,
'title' => 'Translate',
'route_name' => $translation_route_name,
'base_route' => $base_route_name,
) + $base_plugin_definition;
}
return parent::getDerivativeDefinitions($base_plugin_definition);
}
}

View file

@ -0,0 +1,35 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Plugin\views\field\TranslationLink.
*/
namespace Drupal\content_translation\Plugin\views\field;
use Drupal\views\Plugin\views\field\EntityLink;
/**
* Provides a translation link for an entity.
*
* @ingroup views_field_handlers
*
* @ViewsField("content_translation_link")
*/
class TranslationLink extends EntityLink {
/**
* {@inheritdoc}
*/
protected function getEntityLinkTemplate() {
return 'drupal:content-translation-overview';
}
/**
* {@inheritdoc}
*/
protected function getDefaultLabel() {
return $this->t('Translate');
}
}

View file

@ -0,0 +1,179 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Routing\ContentTranslationRouteSubscriber.
*/
namespace Drupal\content_translation\Routing;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Subscriber for entity translation routes.
*/
class ContentTranslationRouteSubscriber extends RouteSubscriberBase {
/**
* The content translation manager.
*
* @var \Drupal\content_translation\ContentTranslationManagerInterface
*/
protected $contentTranslationManager;
/**
* Constructs a ContentTranslationRouteSubscriber object.
*
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
* The content translation manager.
*/
public function __construct(ContentTranslationManagerInterface $content_translation_manager) {
$this->contentTranslationManager = $content_translation_manager;
}
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
foreach ($this->contentTranslationManager->getSupportedEntityTypes() as $entity_type_id => $entity_type) {
// Try to get the route from the current collection.
$link_template = $entity_type->getLinkTemplate('canonical');
if (strpos($link_template, '/') !== FALSE) {
$base_path = '/' . $link_template;
}
else {
if (!$entity_route = $collection->get("entity.$entity_type_id.canonical")) {
continue;
}
$base_path = $entity_route->getPath();
}
// Inherit admin route status from edit route, if exists.
$is_admin = FALSE;
$route_name = "entity.$entity_type_id.edit_form";
if ($edit_route = $collection->get($route_name)) {
$is_admin = (bool) $edit_route->getOption('_admin_route');
}
$path = $base_path . '/translations';
$route = new Route(
$path,
array(
'_controller' => '\Drupal\content_translation\Controller\ContentTranslationController::overview',
'entity_type_id' => $entity_type_id,
),
array(
'_access_content_translation_overview' => $entity_type_id,
),
array(
'parameters' => array(
$entity_type_id => array(
'type' => 'entity:' . $entity_type_id,
),
),
'_admin_route' => $is_admin,
)
);
$route_name = "entity.$entity_type_id.content_translation_overview";
$collection->add($route_name, $route);
$route = new Route(
$path . '/add/{source}/{target}',
array(
'_controller' => '\Drupal\content_translation\Controller\ContentTranslationController::add',
'source' => NULL,
'target' => NULL,
'_title' => 'Add',
'entity_type_id' => $entity_type_id,
),
array(
'_access_content_translation_manage' => 'create',
),
array(
'parameters' => array(
'source' => array(
'type' => 'language',
),
'target' => array(
'type' => 'language',
),
$entity_type_id => array(
'type' => 'entity:' . $entity_type_id,
),
),
'_admin_route' => $is_admin,
)
);
$collection->add("content_translation.translation_add_$entity_type_id", $route);
$route = new Route(
$path . '/edit/{language}',
array(
'_controller' => '\Drupal\content_translation\Controller\ContentTranslationController::edit',
'language' => NULL,
'_title' => 'Edit',
'entity_type_id' => $entity_type_id,
),
array(
'_access_content_translation_manage' => 'update',
),
array(
'parameters' => array(
'language' => array(
'type' => 'language',
),
$entity_type_id => array(
'type' => 'entity:' . $entity_type_id,
),
),
'_admin_route' => $is_admin,
)
);
$collection->add("content_translation.translation_edit_$entity_type_id", $route);
$route = new Route(
$path . '/delete/{language}',
array(
'_entity_form' => $entity_type_id . '.content_translation_deletion',
'language' => NULL,
'_title' => 'Delete',
'entity_type_id' => $entity_type_id,
),
array(
'_access_content_translation_manage' => 'delete',
),
array(
'parameters' => array(
'language' => array(
'type' => 'language',
),
$entity_type_id => array(
'type' => 'entity:' . $entity_type_id,
),
),
'_admin_route' => $is_admin,
)
);
$collection->add("content_translation.translation_delete_$entity_type_id", $route);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events = parent::getSubscribedEvents();
// Should run after AdminRouteSubscriber so the routes can inherit admin
// status of the edit routes on entities. Therefore priority -210.
$events[RoutingEvents::ALTER] = array('onAlterRoutes', -210);
return $events;
}
}

View file

@ -0,0 +1,40 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\ContentTestTranslationUITest.
*/
namespace Drupal\content_translation\Tests;
/**
* Tests the test content translation UI with the test entity.
*
* @group content_translation
*/
class ContentTestTranslationUITest extends ContentTranslationUITestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('language', 'content_translation', 'entity_test');
/**
* Overrides \Drupal\simpletest\WebTestBase::setUp().
*/
protected function setUp() {
// Use the entity_test_mul as this has multilingual property support.
$this->entityTypeId = 'entity_test_mul_changed';
parent::setUp();
}
/**
* Overrides \Drupal\content_translation\Tests\ContentTranslationUITestBase::getTranslatorPermission().
*/
protected function getTranslatorPermissions() {
return array_merge(parent::getTranslatorPermissions(), array('administer entity_test content'));
}
}

View file

@ -0,0 +1,111 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\ContentTranslationConfigImportTest.
*/
namespace Drupal\content_translation\Tests;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\StorageComparer;
use Drupal\simpletest\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;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system', 'user', 'entity_test', 'language', 'content_translation');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test_mul');
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
// Set up the ConfigImporter object for testing.
$storage_comparer = new StorageComparer(
$this->container->get('config.storage.staging'),
$this->container->get('config.storage'),
$this->container->get('config.manager')
);
$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')
);
}
/**
* Tests config import updates.
*/
function testConfigImportUpdates() {
$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');
$staging = $this->container->get('config.storage.staging');
// Verify the configuration to create does not exist yet.
$this->assertIdentical($storage->exists($config_name), FALSE, $config_name . ' not found.');
// Create new config entity.
$data = array(
'uuid' => 'a019d89b-c4d9-4ed4-b859-894e4e2e93cf',
'langcode' => 'en',
'status' => TRUE,
'dependencies' => array(
'module' => array('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' => array(
'content_translation' => array('enabled' => TRUE),
),
);
$staging->write($config_name, $data);
$this->assertIdentical($staging->exists($config_name), TRUE, $config_name . ' found.');
// Import.
$this->configImporter->reset()->import();
// Verify the values appeared.
$config = $this->config($config_name);
$this->assertIdentical($config->get('id'), $config_id);
// Verify that updates were performed.
$entity_type = $this->container->get('entity.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,181 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\ContentTranslationContextualLinksTest.
*/
namespace Drupal\content_translation\Tests;
use Drupal\Component\Serialization\Json;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\NodeType;
use Drupal\simpletest\WebTestBase;
/**
* Tests that contextual links are available for content translation.
*
* @group content_translation
*/
class ContentTranslationContextualLinksTest extends WebTestBase {
/**
* The bundle being tested.
*
* @var string
*/
protected $bundle;
/**
* The content type being tested.
*
* @var NodeType
*/
protected $contentType;
/**
* The 'translator' user to use during testing.
*
* @var \Drupal\user\UserInterface
*/
protected $translator;
/**
* The enabled languages.
*
* @var array
*/
protected $langcodes;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('content_translation', 'contextual', 'node');
/**
* The profile to install as a basis for testing.
*
* @var string
*/
protected $profile = 'testing';
protected function setUp() {
parent::setUp();
// Set up an additional language.
$this->langcodes = array(\Drupal::languageManager()->getDefaultLanguage()->getId(), 'es');
ConfigurableLanguage::createFromLangcode('es')->save();
// Create a content type.
$this->bundle = $this->randomMachineName();
$this->contentType = $this->drupalCreateContentType(array('type' => $this->bundle));
// Add a field to the content type. The field is not yet translatable.
entity_create('field_storage_config', array(
'field_name' => 'field_test_text',
'entity_type' => 'node',
'type' => 'text',
'cardinality' => 1,
))->save();
entity_create('field_config', array(
'entity_type' => 'node',
'field_name' => 'field_test_text',
'bundle' => $this->bundle,
'label' => 'Test text-field',
))->save();
entity_get_form_display('node', $this->bundle, 'default')
->setComponent('field_test_text', array(
'type' => 'text_textfield',
'weight' => 0,
))
->save();
// Create a translator user.
$permissions = array(
'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() {
// Create a node.
$title = $this->randomString();
$this->drupalCreateNode(array('type' => $this->bundle, 'title' => $title, 'langcode' => 'en'));
$node = $this->drupalGetNodeByTitle($title);
// Use a UI form submission to make the node type and field translatable.
// This tests that caches are properly invalidated.
$this->drupalLogin($this->rootUser);
$edit = array(
'entity_types[node]' => TRUE,
'settings[node][' . $this->bundle . '][settings][language][language_alterable]' => TRUE,
'settings[node][' . $this->bundle . '][translatable]' => TRUE,
'settings[node][' . $this->bundle . '][fields][field_test_text]' => TRUE,
);
$this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration'));
$this->drupalLogout();
// Check that the translate link appears on the node page.
$this->drupalLogin($this->translator);
$translate_link = 'node/' . $node->id() . '/translations';
$response = $this->renderContextualLinks(array('node:node=1:'), 'node/' . $node->id());
$this->assertResponse(200);
$json = Json::decode($response);
$this->setRawContent($json['node:node=1:']);
$this->assertLinkByHref($translate_link, 0, 'The contextual link to translate the node is shown.');
// Check that the link leads to the translate page.
$this->drupalGet($translate_link);
$this->assertRaw(t('Translations of %label', array('%label' => $node->label())), 'The contextual link leads to the translate page.');
}
/**
* Get server-rendered contextual links for the given contextual link ids.
*
* Copied from \Drupal\contextual\Tests\ContextualDynamicContextTest::renderContextualLinks().
*
* @param array $ids
* An array of contextual link ids.
* @param string $current_path
* The Drupal path for the page for which the contextual links are rendered.
*
* @return string
* The response body.
*/
protected function renderContextualLinks($ids, $current_path) {
// Build POST values.
$post = array();
for ($i = 0; $i < count($ids); $i++) {
$post['ids[' . $i . ']'] = $ids[$i];
}
// Serialize POST values.
foreach ($post as $key => $value) {
// Encode according to application/x-www-form-urlencoded
// Both names and values needs to be urlencoded, according to
// http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
$post[$key] = urlencode($key) . '=' . urlencode($value);
}
$post = implode('&', $post);
// Perform HTTP request.
return $this->curlExec(array(
CURLOPT_URL => \Drupal::url('contextual.render', array(), array('absolute' => TRUE, 'query' => array('destination' => $current_path))),
CURLOPT_POST => TRUE,
CURLOPT_POSTFIELDS => $post,
CURLOPT_HTTPHEADER => array(
'Accept: application/json',
'Content-Type: application/x-www-form-urlencoded',
),
));
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\ContentTranslationEntityBundleUITest.
*/
namespace Drupal\content_translation\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests the content translation behaviours on entity bundle UI.
*
* @group content_translation
*/
class ContentTranslationEntityBundleUITest extends WebTestBase {
public static $modules = array('language', 'content_translation', 'node', 'comment', 'field_ui');
protected function setUp() {
parent::setUp();
$user = $this->drupalCreateUser(array('access administration pages', 'administer languages', 'administer content translation', 'administer content types'));
$this->drupalLogin($user);
}
/**
* Tests content types default translation behaviour.
*/
public function testContentTypeUI() {
// Create first content type.
$this->drupalCreateContentType(array('type' => 'article'));
// Enable content translation.
$edit = array('language_configuration[content_translation]' => TRUE);
$this->drupalPostForm('admin/structure/types/manage/article', $edit, 'Save content type');
// Make sure add page does not inherit translation configuration from first
// content type.
$this->drupalGet('admin/structure/types/add');
$this->assertNoFieldChecked('edit-language-configuration-content-translation');
// Create second content type and set content translation.
$edit = array(
'name' => 'Page',
'type' => 'page',
'language_configuration[content_translation]' => TRUE,
);
$this->drupalPostForm('admin/structure/types/add', $edit, 'Save and manage fields');
// Make sure the settings are saved when creating the content type.
$this->drupalGet('admin/structure/types/manage/page');
$this->assertFieldChecked('edit-language-configuration-content-translation');
}
}

View file

@ -0,0 +1,148 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\ContentTranslationMetadataFieldsTest.
*/
namespace Drupal\content_translation\Tests;
/**
* 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';
/**
* Modules to install.
*
* @var array
*/
public static $modules = array('language', 'content_translation', 'node');
/**
* The profile to install as a basis for testing.
*
* @var string
*/
protected $profile = 'standard';
/**
* Tests skipping setting non translatable metadata fields.
*/
public function testSkipUntranslatable() {
$this->drupalLogin($this->translator);
$entity_manager = \Drupal::entityManager();
$fields = $entity_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([], $default_langcode);
$storage = $entity_manager->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->assertEqual($created_time, $metadata_target_translation->getCreatedTime(), 'Metadata created field has the same value for both translations.');
$this->assertEqual($changed_time, $metadata_target_translation->getChangedTime(), 'Metadata changed field has the same value for both translations.');
$this->assertEqual($published, $metadata_target_translation->isPublished(), 'Metadata published field has the same value for both translations.');
$this->assertEqual($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->assertEqual($created_time, $metadata_target_translation->getCreatedTime(), 'Metadata created field correctly not updated');
$this->assertEqual($changed_time, $metadata_target_translation->getChangedTime(), 'Metadata changed field correctly not updated');
$this->assertEqual($published, $metadata_target_translation->isPublished(), 'Metadata published field correctly not updated');
$this->assertEqual($author->id(), $metadata_target_translation->getAuthor()->id(), 'Metadata author field correctly not updated');
}
/**
* Tests setting translatable metadata fields.
*/
public function testSetTranslatable() {
$this->drupalLogin($this->translator);
$entity_manager = \Drupal::entityManager();
$fields = $entity_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(['status' => FALSE], $default_langcode);
$storage = $entity_manager->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->assertNotEqual($metadata_source_translation->getCreatedTime(), $metadata_target_translation->getCreatedTime(), 'Metadata created field correctly different on both translations.');
$this->assertNotEqual($metadata_source_translation->getChangedTime(), $metadata_target_translation->getChangedTime(), 'Metadata changed field correctly different on both translations.');
$this->assertNotEqual($metadata_source_translation->isPublished(), $metadata_target_translation->isPublished(), 'Metadata published field correctly different on both translations.');
$this->assertNotEqual($metadata_source_translation->getAuthor()->id(), $metadata_target_translation->getAuthor()->id(), 'Metadata author field correctly different on both translations.');
}
}

View file

@ -0,0 +1,80 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\ContentTranslationOperationsTest.
*/
namespace Drupal\content_translation\Tests;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Tests\NodeTestBase;
/**
* Tests the content translation operations available in the content listing.
*
* @group content_translation
*/
class ContentTranslationOperationsTest extends NodeTestBase {
/**
* A base user.
*
* @var \Drupal\user\Entity\User|false
*/
protected $baseUser1;
/**
* A base user.
*
* @var \Drupal\user\Entity\User|false
*/
protected $baseUser2;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['language', 'content_translation', 'node', 'views'];
protected function setUp() {
parent::setUp();
// Enable additional languages.
$langcodes = ['es', 'ast'];
foreach ($langcodes as $langcode) {
ConfigurableLanguage::createFromLangcode($langcode)->save();
}
// Enable translation for the current entity type and ensure the change is
// picked up.
\Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE);
drupal_static_reset();
\Drupal::entityManager()->clearCachedDefinitions();
\Drupal::service('router.builder')->rebuild();
\Drupal::service('entity.definition_update_manager')->applyUpdates();
$this->baseUser1 = $this->drupalCreateUser(['access content overview']);
$this->baseUser2 = $this->drupalCreateUser(['access content overview', 'create content translations', 'update content translations', 'delete content translations']);
}
/**
* Test that the operation "Translate" is displayed in the content listing.
*/
function testOperationTranslateLink() {
$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->assertNoLinkByHref('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->assertLinkByHref('node/' . $node->id() . '/translations');
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\ContentTranslationSettingsApiTest.
*/
namespace Drupal\content_translation\Tests;
use Drupal\simpletest\KernelTestBase;
/**
* Tests the content translation settings API.
*
* @group content_translation
*/
class ContentTranslationSettingsApiTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('language', 'content_translation', 'user', 'entity_test');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test_mul');
}
/**
* Tests that enabling translation via the API triggers schema updates.
*/
function testSettingsApi() {
$this->container->get('content_translation.manager')->setEnabled('entity_test_mul', 'entity_test_mul', TRUE);
$result =
db_field_exists('entity_test_mul_property_data', 'content_translation_source') &&
db_field_exists('entity_test_mul_property_data', 'content_translation_outdated');
$this->assertTrue($result, 'Schema updates correctly performed.');
}
}

View file

@ -0,0 +1,298 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\ContentTranslationSettingsTest.
*/
namespace Drupal\content_translation\Tests;
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\simpletest\WebTestBase;
/**
* Tests the content translation settings UI.
*
* @group content_translation
*/
class ContentTranslationSettingsTest extends WebTestBase {
use CommentTestTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('language', 'content_translation', 'node', 'comment', 'field_ui', 'entity_test');
protected function setUp() {
parent::setUp();
// Set up two content types to test fields shared between different
// bundles.
$this->drupalCreateContentType(array('type' => 'article'));
$this->drupalCreateContentType(array('type' => 'page'));
$this->addDefaultCommentField('node', 'article', 'comment_article', CommentItemInterface::OPEN, 'comment_article');
$this->addDefaultCommentField('node', 'page', 'comment_page');
$admin_user = $this->drupalCreateUser(array('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.
*/
function testSettingsUI() {
// Check for the content_translation_menu_links_discovered_alter() changes.
$this->drupalGet('admin/config');
$this->assertLink('Content language and translation');
$this->assertText('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 = array('settings[comment][comment_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_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_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);
$xpath_err = '//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_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->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_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);
$definition = $this->entityManager()->getFieldDefinitions('comment', 'comment_article')['comment_body'];
$this->assertTrue($definition->isTranslatable(), 'Article comment body is translatable.');
$definition = $this->entityManager()->getFieldDefinitions('comment', 'comment_article')['subject'];
$this->assertFalse($definition->isTranslatable(), 'Article comment subject is not translatable.');
$definition = $this->entityManager()->getFieldDefinitions('comment', 'comment')['comment_body'];
$this->assertFalse($definition->isTranslatable(), 'Page comment body is not translatable.');
$definition = $this->entityManager()->getFieldDefinitions('comment', 'comment')['subject'];
$this->assertFalse($definition->isTranslatable(), 'Page comment subject is not translatable.');
// Test that translation can be enabled for base fields.
$edit = array(
'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 = $this->entityManager()->getFieldDefinitions('entity_test_mul', 'entity_test_mul');
$this->assertTrue($definitions['name']->isTranslatable() && !$definitions['user_id']->isTranslatable(), 'Base field bundle overrides were correctly altered.');
// Test that language settings are correctly stored.
$language_configuration = ContentLanguageSettings::loadByEntityTypeBundle('comment', 'comment_article');
$this->assertEqual($language_configuration->getDefaultLangcode(), 'current_interface', '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->assertField('language_configuration[content_translation]');
$this->assertFieldChecked('edit-language-configuration-content-translation');
// Verify that translation may be enabled for the article content type.
$edit = array(
'language_configuration[content_translation]' => TRUE,
);
// Make sure the checkbox is available and not checked by default.
$this->drupalGet('admin/structure/types/manage/article');
$this->assertField('language_configuration[content_translation]');
$this->assertNoFieldChecked('edit-language-configuration-content-translation');
$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_alterable]' => TRUE,
'settings[node][article][translatable]' => TRUE,
'settings[node][article][fields][title]' => TRUE
);
$this->assertSettings('node', NULL, TRUE, $edit);
foreach (array(TRUE, FALSE) as $translatable) {
// Test that configurable field translatability is correctly switched.
$edit = array('settings[node][article][fields][body]' => $translatable);
$this->assertSettings('node', 'article', TRUE, $edit);
$field = FieldConfig::loadByName('node', 'article', 'body');
$definitions = \Drupal::entityManager()->getFieldDefinitions('node', 'article');
$this->assertEqual($definitions['body']->isTranslatable(), $translatable, 'Field translatability correctly switched.');
$this->assertEqual($field->isTranslatable(), $definitions['body']->isTranslatable(), 'Configurable field translatability correctly switched.');
// Test that also the Field UI form behaves correctly.
$translatable = !$translatable;
$edit = array('translatable' => $translatable);
$this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.body', $edit, t('Save settings'));
\Drupal::entityManager()->clearCachedFieldDefinitions();
$field = FieldConfig::loadByName('node', 'article', 'body');
$definitions = \Drupal::entityManager()->getFieldDefinitions('node', 'article');
$this->assertEqual($definitions['body']->isTranslatable(), $translatable, 'Field translatability correctly switched.');
$this->assertEqual($field->isTranslatable(), $definitions['body']->isTranslatable(), 'Configurable field translatability correctly switched.');
}
// 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 = array(
'site_default',
'current_interface',
'authors_default',
'en',
'und',
'zxx',
);
$elements = $this->xpath('//select[@id="edit-settings-node-article-settings-language-langcode"]/option');
// Compare values inside the option elements with expected values.
for ($i = 0; $i < count($elements); $i++) {
$this->assertEqual($elements[$i]->attributes()->{'value'}, $expected_elements[$i]);
}
}
/**
* Tests the language settings checkbox on account settings page.
*/
function testAccountLanguageSettingsUI() {
// Make sure the checkbox is available and not checked by default.
$this->drupalGet('admin/config/people/accounts');
$this->assertField('language[content_translation]');
$this->assertNoFieldChecked('edit-language-content-translation');
$edit = array(
'language[content_translation]' => TRUE,
);
$this->drupalPostForm('admin/config/people/accounts', $edit, t('Save configuration'));
$this->drupalGet('admin/config/people/accounts');
$this->assertFieldChecked('edit-language-content-translation');
}
/**
* 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 $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.
*
* @return boolean
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertSettings($entity_type, $bundle, $enabled, $edit) {
$this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration'));
$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::entityManager()->clearCachedDefinitions();
return $this->assertEqual(\Drupal::service('content_translation.manager')->isEnabled($entity_type, $bundle), $enabled, $message);
}
/**
* Tests that field setting depends on bundle translatability.
*/
function testFieldTranslatableSettingsUI() {
// 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.
$edit = array(
'new_storage_type' => 'text',
'label' => 'Test',
'field_name' => 'article_text',
);
$this->drupalPostForm('admin/structure/types/manage/article/fields/add-field', $edit, 'Save and continue');
// 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->assertFieldByXPath('//input[@id="edit-translatable" and @disabled="disabled"]');
$this->assertText('To configure translation for this field, enable language support for this type.', 'No translatable setting for field.');
// Tests that field has translatable setting if bundle is translatable.
// Note: this field is not translatable when enable bundle translatability.
$edit = array(
'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->assertFieldByXPath('//input[@id="edit-translatable" and not(@disabled) and @checked="checked"]');
$this->assertNoText('To enable translation of this field, enable language support for this type.', 'Translatable setting for field available.');
}
/**
* Tests the translatable settings checkbox for untranslatable entities.
*/
function testNonTranslatableTranslationSettingsUI() {
$this->drupalGet('admin/config/regional/content-language');
$this->assertNoField('settings[entity_test][entity_test][translatable]');
}
/**
* Returns the entity manager.
*
* @return \Drupal\Core\Entity\EntityManagerInterface
* The entity manager;
*/
protected function entityManager() {
return $this->container->get('entity.manager');
}
}

View file

@ -0,0 +1,80 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\ContentTranslationStandardFieldsTest.
*/
namespace Drupal\content_translation\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests the Content translation settings using the standard profile.
*
* @group content_translation
*/
class ContentTranslationStandardFieldsTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array(
'language',
'content_translation',
'node',
'comment',
'field_ui',
'entity_test',
);
/**
* {@inheritdoc}
*/
protected $profile = 'standard';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$admin_user = $this->drupalCreateUser(array(
'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() {
$path = 'admin/config/regional/content-language';
$this->drupalGet($path);
// Check content block fields.
$this->assertFieldByXPath("//input[@id='edit-settings-block-content-basic-fields-body' and @checked='checked']");
// Check comment fields.
$this->assertFieldByXPath("//input[@id='edit-settings-comment-comment-fields-comment-body' and @checked='checked']");
// Check node fields.
$this->assertFieldByXPath("//input[@id='edit-settings-node-article-fields-comment' and @checked='checked']");
$this->assertFieldByXPath("//input[@id='edit-settings-node-article-fields-field-image' and @checked='checked']");
$this->assertFieldByXPath("//input[@id='edit-settings-node-article-fields-field-tags' and @checked='checked']");
// Check user fields.
$this->assertFieldByXPath("//input[@id='edit-settings-user-user-fields-user-picture' and @checked='checked']");
}
}

View file

@ -0,0 +1,250 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\ContentTranslationSyncImageTest.
*/
namespace Drupal\content_translation\Tests;
use Drupal\Core\Entity\EntityInterface;
/**
* Tests the field synchronization behavior for the image field.
*
* @group content_translation
*/
class ContentTranslationSyncImageTest extends ContentTranslationTestBase {
/**
* The cardinality of the image field.
*
* @var int
*/
protected $cardinality;
/**
* The test image files.
*
* @var array
*/
protected $files;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('language', 'content_translation', 'entity_test', 'image', 'field_ui');
protected function setUp() {
parent::setUp();
$this->files = $this->drupalGetTestFiles('image');
}
/**
* Creates the test image field.
*/
protected function setupTestFields() {
$this->fieldName = 'field_test_et_ui_image';
$this->cardinality = 3;
entity_create('field_storage_config', array(
'field_name' => $this->fieldName,
'entity_type' => $this->entityTypeId,
'type' => 'image',
'cardinality' => $this->cardinality,
))->save();
entity_create('field_config', array(
'entity_type' => $this->entityTypeId,
'field_name' => $this->fieldName,
'bundle' => $this->entityTypeId,
'label' => 'Test translatable image field',
'third_party_settings' => array(
'content_translation' => array(
'translation_sync' => array(
'file' => FALSE,
'alt' => 'alt',
'title' => 'title',
),
),
),
))->save();
}
/**
* {@inheritdoc}
*/
protected function getEditorPermissions() {
// Every entity-type-specific test needs to define these.
return array('administer entity_test_mul fields', 'administer languages', 'administer content translation');
}
/**
* Tests image field field synchronization.
*/
function testImageFieldSync() {
// 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->assertFieldChecked('edit-third-party-settings-content-translation-translation-sync-alt');
$this->assertFieldChecked('edit-third-party-settings-content-translation-translation-sync-title');
$edit = array(
'third_party_settings[content_translation][translation_sync][alt]' => FALSE,
'third_party_settings[content_translation][translation_sync][title]' => FALSE,
);
$this->drupalPostForm(NULL, $edit, t('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->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 configuration'));
$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);
$default_langcode = $this->langcodes[0];
$langcode = $this->langcodes[1];
// Populate the test entity with some random initial values.
$values = array(
'name' => $this->randomMachineName(),
'user_id' => mt_rand(1, 128),
'langcode' => $default_langcode,
);
$entity = entity_create($this->entityTypeId, $values);
// Create some file entities from the generated test files and store them.
$values = array();
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 = array(
'uri' => $this->files[$index]->uri,
'uid' => \Drupal::currentUser()->id(),
'status' => FILE_STATUS_PERMANENT,
);
$file = entity_create('file', $field_values);
$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 = array(
'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->getTranslation($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 = array(
'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 = array();
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, format_string('Field item @fid has been successfully synchronized.', array('@fid' => $item->target_id)));
$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]), format_string('Field item @fid has been correctly removed.', array('@fid' => $removed_fid)));
// Add back an item for the dropped value and perform synchronization again.
$values[$langcode][$removed_fid] = array(
'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, format_string('Field item @fid has been successfully synchronized.', array('@fid' => $item->target_id)));
}
}
/**
* 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) {
$entity->save();
$entity = entity_test_mul_load($entity->id(), TRUE);
return $entity;
}
}

View file

@ -0,0 +1,255 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\ContentTranslationSyncUnitTest.
*/
namespace Drupal\content_translation\Tests;
use Drupal\simpletest\KernelTestBase;
use Drupal\content_translation\FieldTranslationSynchronizer;
/**
* 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 integer
*/
protected $cardinality;
/**
* The unchanged field values.
*
* @var array
*/
protected $unchangedFieldValues;
public static $modules = array('language', 'content_translation');
protected function setUp() {
parent::setUp();
$this->synchronizer = new FieldTranslationSynchronizer($this->container->get('entity.manager'));
$this->synchronized = array('sync1', 'sync2');
$this->columns = array_merge($this->synchronized, array('var1', 'var2'));
$this->langcodes = array('en', 'it', 'fr', 'de', 'es');
$this->cardinality = 4;
$this->unchangedFieldValues = array();
// 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() {
// 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 = array();
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] = array();
// 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() {
$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 = array(
// 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) {
$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);
$result = TRUE;
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.
$value = $delta > 0 && $delta_callback($delta) ? $changed_items[0][$column] : $unchanged_items[$delta][$column];
$result = $result && ($field_values[$langcode][$delta][$column] == $value);
}
}
}
$this->assertTrue($result, 'Multiple synced items have been correctly synchronized.');
}
}
/**
* Tests that one change in a synchronized column triggers a change in all columns.
*/
public function testDifferingSyncedColumns() {
$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);
$result = TRUE;
foreach ($this->unchangedFieldValues as $langcode => $unchanged_items) {
for ($delta = 0; $delta < $this->cardinality; $delta++) {
foreach ($this->columns as $column) {
$result = $result && ($field_values[$langcode][$delta][$column] == $changed_items[$delta][$column]);
}
}
}
$this->assertTrue($result, 'Differing synced columns have been correctly synchronized.');
}
}

View file

@ -0,0 +1,240 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\ContentTranslationTestBase.
*/
namespace Drupal\content_translation\Tests;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\simpletest\WebTestBase;
/**
* Base class for content translation tests.
*/
abstract class ContentTranslationTestBase extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('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;
protected function setUp() {
parent::setUp();
$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 = array('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(array($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::entityManager()->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 array();
}
/**
* Returns an array of permissions needed for the administrator.
*/
protected function getAdministratorPermissions() {
return array_merge($this->getEditorPermissions(), $this->getTranslatorPermissions(), array('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);
drupal_static_reset();
\Drupal::entityManager()->clearCachedDefinitions();
\Drupal::service('router.builder')->rebuild();
\Drupal::service('entity.definition_update_manager')->applyUpdates();
}
/**
* Creates the test fields.
*/
protected function setupTestFields() {
if (empty($this->fieldName)) {
$this->fieldName = 'field_test_et_ui_test';
}
entity_create('field_storage_config', array(
'field_name' => $this->fieldName,
'type' => 'string',
'entity_type' => $this->entityTypeId,
'cardinality' => 1,
))->save();
entity_create('field_config', array(
'entity_type' => $this->entityTypeId,
'field_name' => $this->fieldName,
'bundle' => $this->bundle,
'label' => 'Test translatable text-field',
))->save();
entity_get_form_display($this->entityTypeId, $this->bundle, 'default')
->setComponent($this->fieldName, array(
'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::entityManager()->getDefinition($this->entityTypeId);
if ($bundle_key = $entity_type->getKey('bundle')) {
$entity_values[$bundle_key] = $bundle_name ?: $this->bundle;
}
$controller = $this->container->get('entity.manager')->getStorage($this->entityTypeId);
if (!($controller instanceof SqlContentEntityStorage)) {
foreach ($values as $property => $value) {
if (is_array($value)) {
$entity_values[$property] = array($langcode => $value);
}
}
}
$entity = entity_create($this->entityTypeId, $entity_values);
$entity->save();
return $entity->id();
}
}

View file

@ -0,0 +1,44 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\ContentTranslationUISkipTest.
*/
namespace Drupal\content_translation\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests the content translation UI check skip.
*
* @group content_translation
*/
class ContentTranslationUISkipTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('content_translation_test', 'user', 'node');
/**
* Tests the content_translation_ui_skip key functionality.
*/
function testUICheckSkip() {
$admin_user = $this->drupalCreateUser(array(
'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->assertText('Test entity - Translatable skip UI check');
$this->assertText('Test entity - Translatable check UI (Translation is not supported)');
}
}

View file

@ -0,0 +1,511 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\ContentTranslationUITestBase.
*/
namespace Drupal\content_translation\Tests;
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\Component\Utility\SafeMarkup;
/**
* Tests the Content Translation UI.
*/
abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
/**
* 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;
/**
* Tests the basic translation UI.
*/
function testTranslationUI() {
$this->doTestBasicTranslation();
$this->doTestTranslationOverview();
$this->doTestOutdatedStatus();
$this->doTestPublishedStatus();
$this->doTestAuthoringInfo();
$this->doTestTranslationEdit();
$this->doTestTranslationChanged();
$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);
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$this->assertTrue($entity, 'Entity found in the database.');
$this->drupalGet($entity->urlInfo());
$this->assertResponse(200, 'Entity URL is valid.');
$this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
$this->assertNoText('Source language', 'Source language column correctly hidden.');
$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 = format_string('@property correctly stored in the default language.', array('@property' => $property));
$this->assertEqual($stored_value, $value, $message);
}
// Add a content translation.
$langcode = 'it';
$language = ConfigurableLanguage::load($langcode);
$values[$langcode] = $this->getNewEntityValues($langcode);
$add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [
$entity->getEntityTypeId() => $entity->id(),
'source' => $default_langcode,
'target' => $langcode
], array('language' => $language));
$this->drupalPostForm($add_url, $this->getEditValues($values, $langcode), $this->getFormSubmitActionForNewTranslation($entity, $langcode));
// Get the entity and reset its cache, so that the new translation gets the
// updated values.
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$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->assertEqual($metadata_target_translation->getAuthor()->id(), $this->translator->id(),
SafeMarkup::format('Author of the target translation @langcode correctly stored for translatable owner field.', array('@langcode' => $langcode)));
$this->assertNotEqual($metadata_target_translation->getAuthor()->id(), $metadata_source_translation->getAuthor()->id(),
SafeMarkup::format('Author of the target translation @target different from the author of the source translation @source for translatable owner field.',
array('@target' => $langcode, '@source' => $default_langcode)));
}
else {
$this->assertEqual($metadata_target_translation->getAuthor()->id(), $this->editor->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()) {
$this->assertTrue($metadata_target_translation->getCreatedTime() > $metadata_source_translation->getCreatedTime(),
SafeMarkup::format('Translation creation timestamp of the target translation @target is newer than the creation timestamp of the source translation @source for translatable created field.',
array('@target' => $langcode, '@source' => $default_langcode)));
}
else {
$this->assertEqual($metadata_target_translation->getCreatedTime(), $metadata_source_translation->getCreatedTime(), 'Creation timestamp of the entity remained untouched after translation for non translatable created field.');
}
if ($this->testLanguageSelector) {
$this->assertNoFieldByXPath('//select[@id="edit-langcode-0-value"]', NULL, 'Language selector correctly disabled on translations.');
}
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
$this->assertNoText('Source language', 'Source language column correctly hidden.');
// Switch the source language.
$langcode = 'fr';
$language = ConfigurableLanguage::load($langcode);
$source_langcode = 'it';
$edit = array('source_langcode[source]' => $source_langcode);
$add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [
$entity->getEntityTypeId() => $entity->id(),
'source' => $default_langcode,
'target' => $langcode
], array('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->drupalPostForm($add_url, $edit, t('Change'));
$this->assertFieldByXPath("//input[@name=\"{$this->fieldName}[0][value]\"]", $values[$source_langcode][$this->fieldName][0]['value'], 'Source language correctly switched.');
// Add another translation and mark the other ones as outdated.
$values[$langcode] = $this->getNewEntityValues($langcode);
$edit = $this->getEditValues($values, $langcode) + array('content_translation[retranslate]' => TRUE);
$add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [
$entity->getEntityTypeId() => $entity->id(),
'source' => $source_langcode,
'target' => $langcode
], array('language' => $language));
$this->drupalPostForm($add_url, $edit, $this->getFormSubmitActionForNewTranslation($entity, $langcode));
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
$this->assertText('Source language', 'Source language column correctly shown.');
// 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 = format_string('%property correctly stored with language %language.', array('%property' => $property, '%language' => $langcode));
$this->assertEqual($stored_value, $value, $message);
}
}
}
/**
* Tests that the translation overview shows the correct values.
*/
protected function doTestTranslationOverview() {
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
foreach ($this->langcodes as $langcode) {
if ($entity->hasTranslation($langcode)) {
$language = new Language(array('id' => $langcode));
$view_path = $entity->url('canonical', array('language' => $language));
$elements = $this->xpath('//table//a[@href=:href]', array(':href' => $view_path));
$this->assertEqual((string) $elements[0], $entity->getTranslation($langcode)->label(), format_string('Label correctly shown for %language translation.', array('%language' => $langcode)));
$edit_path = $entity->url('edit-form', array('language' => $language));
$elements = $this->xpath('//table//ul[@class="dropbutton"]/li/a[@href=:href]', array(':href' => $edit_path));
$this->assertEqual((string) $elements[0], t('Edit'), format_string('Edit link correct for %language translation.', array('%language' => $langcode)));
}
}
}
/**
* Tests up-to-date status tracking.
*/
protected function doTestOutdatedStatus() {
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$langcode = 'fr';
$languages = \Drupal::languageManager()->getLanguages();
// Mark translations as outdated.
$edit = array('content_translation[retranslate]' => TRUE);
$edit_path = $entity->urlInfo('edit-form', array('language' => $languages[$langcode]));
$this->drupalPostForm($edit_path, $edit, $this->getFormSubmitAction($entity, $langcode));
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
// 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->urlInfo('edit-form', array('language' => ConfigurableLanguage::load($added_langcode)));
$this->drupalGet($url);
if ($added_langcode == $langcode) {
$this->assertFieldByXPath('//input[@name="content_translation[retranslate]"]', FALSE, 'The retranslate flag is not checked by default.');
$this->assertFalse($this->xpath('//details[@id="edit-content-translation" and @open="open"]'), 'The translation tab should be collapsed by default.');
}
else {
$this->assertFieldByXPath('//input[@name="content_translation[outdated]"]', TRUE, 'The translate flag is checked by default.');
$this->assertTrue($this->xpath('//details[@id="edit-content-translation" and @open="open"]'), 'The translation tab is correctly expanded when the translation is outdated.');
$edit = array('content_translation[outdated]' => FALSE);
$this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $added_langcode));
$this->drupalGet($url);
$this->assertFieldByXPath('//input[@name="content_translation[retranslate]"]', FALSE, 'The retranslate flag is now shown.');
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$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() {
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
// Unpublish translations.
foreach ($this->langcodes as $index => $langcode) {
if ($index > 0) {
$url = $entity->urlInfo('edit-form', array('language' => ConfigurableLanguage::load($langcode)));
$edit = array('content_translation[status]' => FALSE);
$this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $langcode));
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$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->urlInfo('edit-form'));
$this->assertFieldByXPath('//input[@name="content_translation[status]" and @disabled="disabled"]', TRUE, 'The last translation is published and cannot be unpublished.');
}
/**
* Tests the translation authoring information.
*/
protected function doTestAuthoringInfo() {
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$values = array();
// Post different authoring information for each translation.
foreach ($this->langcodes as $index => $langcode) {
$user = $this->drupalCreateUser();
$values[$langcode] = array(
'uid' => $user->id(),
'created' => REQUEST_TIME - mt_rand(0, 1000),
);
$edit = array(
'content_translation[uid]' => $user->getUsername(),
'content_translation[created]' => format_date($values[$langcode]['created'], 'custom', 'Y-m-d H:i:s O'),
);
$url = $entity->urlInfo('edit-form', array('language' => ConfigurableLanguage::load($langcode)));
$this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $langcode));
}
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
foreach ($this->langcodes as $langcode) {
$metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
$this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly stored.');
$this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly stored.');
}
// Try to post non valid values and check that they are rejected.
$langcode = 'en';
$edit = array(
// User names have by default length 8.
'content_translation[uid]' => $this->randomMachineName(12),
'content_translation[created]' => '19/11/1978',
);
$this->drupalPostForm($entity->urlInfo('edit-form'), $edit, $this->getFormSubmitAction($entity, $langcode));
$this->assertTrue($this->xpath('//div[contains(concat(" ", normalize-space(@class), " "), :class)]', array(':class' => ' messages--error ')), 'Invalid values generate a form error message.');
$metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
$this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly kept.');
$this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly kept.');
}
/**
* Tests translation deletion.
*/
protected function doTestTranslationDeletion() {
// Confirm and delete a translation.
$this->drupalLogin($this->translator);
$langcode = 'fr';
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$language = ConfigurableLanguage::load($langcode);
$url = $entity->urlInfo('edit-form', array('language' => $language));
$this->drupalPostForm($url, array(), t('Delete translation'));
$this->drupalPostForm(NULL, array(), t('Delete @language translation', array('@language' => $language->getName())));
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
if ($this->assertTrue(is_object($entity), 'Entity found')) {
$translations = $entity->getTranslationLanguages();
$this->assertTrue(count($translations) == 2 && empty($translations[$langcode]), 'Translation successfully deleted.');
}
// Check that the translator cannot delete the original translation.
$args = [$this->entityTypeId => $entity->id(), 'language' => 'en'];
$this->drupalGet(Url::fromRoute('content_translation.translation_delete_' . $this->entityTypeId, $args));
$this->assertResponse(403);
}
/**
* Returns an array of entity field values to be tested.
*/
protected function getNewEntityValues($langcode) {
return array($this->fieldName => array(array('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 t('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
* 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 the field name.
*/
protected function getChangedFieldName($entity) {
return $entity->hasField('content_translation_changed') ? 'content_translation_changed' : 'changed';
}
/**
* Tests edit content translation.
*/
protected function doTestTranslationEdit() {
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$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 = array('language' => $languages[$langcode]);
$url = $entity->urlInfo('edit-form', $options);
$this->drupalGet($url);
$this->assertRaw($entity->getTranslation($langcode)->label());
}
}
}
/**
* Tests the basic translation workflow.
*/
protected function doTestTranslationChanged() {
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$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 = array(
$this->fieldName . '[0][value]' => $this->randomString(),
);
$edit_path = $entity->urlInfo('edit-form', array('language' => $language));
$this->drupalPostForm($edit_path, $edit, $this->getFormSubmitAction($entity, $langcode));
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$this->assertEqual(
$entity->getChangedTimeAcrossTranslations(), $entity->getTranslation($langcode)->getChangedTime(),
format_string('Changed time for language %language is the latest change over all languages.', array('%language' => $language->getName()))
);
}
$timestamps = array();
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->assertEqual(
count($timestamps), count($entity->getTranslationLanguages()),
'All timestamps from all languages are different.'
);
}
else {
$this->assertEqual(
count($timestamps), 1,
'All timestamps from all languages are identical.'
);
}
}
}
}

View file

@ -0,0 +1,241 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\ContentTranslationWorkflowsTest.
*/
namespace Drupal\content_translation\Tests;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\user\UserInterface;
/**
* Tests the content translation workflows for the test entity.
*
* @group content_translation
*/
class ContentTranslationWorkflowsTest extends ContentTranslationTestBase {
/**
* The entity used for testing.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $entity;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('language', 'content_translation', 'entity_test');
protected function setUp() {
parent::setUp();
$this->setupEntity();
}
/**
* Overrides \Drupal\content_translation\Tests\ContentTranslationTestBase::getEditorPermissions().
*/
protected function getEditorPermissions() {
return array('administer entity_test content');
}
/**
* Creates a test entity and translate it.
*/
protected function setupEntity() {
$default_langcode = $this->langcodes[0];
// Create a test entity.
$user = $this->drupalCreateUser();
$values = array(
'name' => $this->randomMachineName(),
'user_id' => $user->id(),
$this->fieldName => array(array('value' => $this->randomMachineName(16))),
);
$id = $this->createEntity($values, $default_langcode);
$this->entity = entity_load($this->entityTypeId, $id, TRUE);
// Create a translation.
$this->drupalLogin($this->translator);
$add_translation_url = Url::fromRoute('content_translation.translation_add_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $this->langcodes[2]]);
$this->drupalPostForm($add_translation_url, array(), t('Save'));
$this->rebuildContainer();
}
/**
* Test simple and editorial translation workflows.
*/
function testWorkflows() {
// Test workflows for the editor.
$expected_status = [
'edit' => 200,
'delete' => 200,
'overview' => 403,
'add_translation' => 403,
'edit_translation' => 403,
'delete_translation' => 403,
];
$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,
];
$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,
];
$this->doTestWorkflows($this->administrator, $expected_status);
// Check that translation permissions allow the associated operations.
$ops = array('create' => t('Add'), 'update' => t('Edit'), 'delete' => t('Delete'));
$translations_url = $this->entity->urlInfo('drupal:content-translation-overview');
foreach ($ops as $current_op => $item) {
$user = $this->drupalCreateUser(array($this->getTranslatePermission(), "$current_op content translations"));
$this->drupalLogin($user);
$this->drupalGet($translations_url);
foreach ($ops as $op => $label) {
if ($op != $current_op) {
$this->assertNoLink($label, format_string('No %op link found.', array('%op' => $label)));
}
else {
$this->assertLink($label, 0, format_string('%op link found.', array('%op' => $label)));
}
}
}
}
/**
* 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) {
$default_langcode = $this->langcodes[0];
$languages = $this->container->get('language_manager')->getLanguages();
$args = ['@user_label' => $user->getUsername()];
$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->urlInfo('edit-form', $options);
$this->drupalGet($edit_url, $options);
$this->assertResponse($expected_status['edit'], SafeMarkup::format('The @user_label has the expected edit access.', $args));
// Check whether the user is allowed to access the entity delete form.
$delete_url = $this->entity->urlInfo('delete-form', $options);
$this->drupalGet($delete_url, $options);
$this->assertResponse($expected_status['delete'], SafeMarkup::format('The @user_label has the expected delete access.', $args));
// Check whether the user is allowed to access the translation overview.
$langcode = $this->langcodes[1];
$options['language'] = $languages[$langcode];
$translations_url = $this->entity->url('drupal:content-translation-overview', $options);
$this->drupalGet($translations_url);
$this->assertResponse($expected_status['overview'], SafeMarkup::format('The @user_label has the expected translation overview access.', $args));
// Check whether the user is allowed to create a translation.
$add_translation_url = Url::fromRoute('content_translation.translation_add_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $langcode], $options);
if ($expected_status['add_translation'] == 200) {
$this->clickLink('Add');
$this->assertUrl($add_translation_url->toString(), [], 'The translation overview points to the translation form when creating translations.');
// 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->assertResponse($expected_status['add_translation'], SafeMarkup::format('The @user_label has the expected translation creation access.', $args));
// Check whether the user is allowed to edit a translation.
$langcode = $this->langcodes[2];
$options['language'] = $languages[$langcode];
$edit_translation_url = Url::fromRoute('content_translation.translation_edit_' . $this->entityTypeId, [$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', 2);
// 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->url('edit-form', $options);
$this->assertUrl($expected_edit_path, [], 'The translation overview points to the edit form for editors when editing translations.');
}
else {
$this->clickLink('Edit');
// While a translator should be pointed to the translation form.
$this->assertUrl($edit_translation_url->toString(), [], 'The translation overview points to the translation form for translators when editing translations.');
// Check that the translation form does not contain shared elements.
$this->assertNoSharedElements();
}
}
else {
$this->drupalGet($edit_translation_url);
}
$this->assertResponse($expected_status['edit_translation'], SafeMarkup::format('The @user_label has the expected translation edit access.', $args));
// Check whether the user is allowed to delete a translation.
$langcode = $this->langcodes[2];
$options['language'] = $languages[$langcode];
$delete_translation_url = Url::fromRoute('content_translation.translation_delete_' . $this->entityTypeId, [$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', 2);
// 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->url('delete-form', $options);
$this->assertUrl($expected_delete_path, [], 'The translation overview points to the delete form for editors when deleting translations.');
}
else {
$this->clickLink('Delete');
// While a translator should be pointed to the translation deletion
// form.
$this->assertUrl($delete_translation_url->toString(), [], 'The translation overview points to the translation deletion form for translators when deleting translations.');
}
}
else {
$this->drupalGet($delete_translation_url);
}
$this->assertResponse($expected_status['delete_translation'], SafeMarkup::format('The @user_label has the expected translation deletion access.', $args));
}
/**
* Assert that the current page does not contain shared form elements.
*/
protected function assertNoSharedElements() {
$language_none = LanguageInterface::LANGCODE_NOT_SPECIFIED;
return $this->assertNoFieldByXPath("//input[@name='field_test_text[$language_none][0][value]']", NULL, 'Shared elements are not available on the translation form.');
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\Views\ContentTranslationViewsUITest.
*/
namespace Drupal\content_translation\Tests\Views;
use Drupal\views_ui\Tests\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 = array('test_view');
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('content_translation');
/**
* Tests the views UI.
*/
public function testViewsUI() {
$this->drupalGet('admin/structure/views/view/test_view/edit');
$this->assertTitle(t('@label (@table) | @site-name', array('@label' => 'Test view', '@table' => 'Views test data', '@site-name' => $this->config('system.site')->get('name'))));
}
}

View file

@ -0,0 +1,66 @@
<?php
/**
* @file
* Contains \Drupal\content_translation\Tests\Views\TranslationLinkTest.
*/
namespace Drupal\content_translation\Tests\Views;
use Drupal\views\Tests\ViewTestBase;
use Drupal\content_translation\Tests\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 = array('test_entity_translations_link');
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('content_translation_test_views');
protected function setUp() {
// @todo Use entity_type once it is has multilingual Views integration.
$this->entityTypeId = 'user';
parent::setUp();
// 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(get_class($this), array('content_translation_test_views'));
}
/**
* Tests the content translation overview link field handler.
*/
public function testTranslationLink() {
$this->drupalGet('test-entity-translations-link');
$this->assertLinkByHref('user/1/translations');
$this->assertNoLinkByHref('user/2/translations', 'The translations link is not present when content_translation_translate_access() is FALSE.');
}
}

View file

@ -0,0 +1,10 @@
name: 'Content translation tests'
type: module
description: 'Provides content translation tests.'
package: Testing
version: VERSION
core: 8.x
dependencies:
- content_translation
- language
- entity_test

View file

@ -0,0 +1,32 @@
<?php
/**
* @file
* Contains \Drupal\content_translation_test\Entity\EntityTestTranslatableNoUISkip.
*/
namespace Drupal\content_translation_test\Entity;
use Drupal\entity_test\Entity\EntityTest;
/**
* Defines the test entity class.
*
* @ContentEntityType(
* id = "entity_test_translatable_no_skip",
* label = @Translation("Test entity - Translatable check UI"),
* base_table = "entity_test_mul",
* data_table = "entity_test_mul_property_data",
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid",
* "bundle" = "type",
* "label" = "name",
* "langcode" = "langcode",
* },
* translatable = TRUE,
* )
*/
class EntityTestTranslatableNoUISkip extends EntityTest {
}

View file

@ -0,0 +1,33 @@
<?php
/**
* @file
* Contains \Drupal\content_translation_test\Entity\EntityTestTranslatableUISkip.
*/
namespace Drupal\content_translation_test\Entity;
use Drupal\entity_test\Entity\EntityTest;
/**
* Defines the test entity class.
*
* @ContentEntityType(
* id = "entity_test_translatable_UI_skip",
* label = @Translation("Test entity - Translatable skip UI check"),
* base_table = "entity_test_mul",
* data_table = "entity_test_mul_property_data",
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid",
* "bundle" = "type",
* "label" = "name",
* "langcode" = "langcode",
* },
* translatable = TRUE,
* content_translation_ui_skip = TRUE,
* )
*/
class EntityTestTranslatableUISkip extends EntityTest {
}

View file

@ -0,0 +1,9 @@
name: 'Content translation test views'
type: module
description: 'Provides default views for views content translation tests.'
package: Testing
version: VERSION
core: 8.x
dependencies:
- content_translation
- 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
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
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:
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,104 @@
<?php
/**
* @file
* Contains \Drupal\Tests\content_translation\Unit\Access\ContentTranslationManageAccessCheckTest.
*/
namespace Drupal\Tests\content_translation\Unit\Access;
use Drupal\content_translation\Access\ContentTranslationManageAccessCheck;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Language\Language;
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 {
/**
* Tests the create access method.
*
* @covers ::access
*/
public function testCreateAccess() {
// Set the mock translation handler.
$translation_handler = $this->getMock('\Drupal\content_translation\ContentTranslationHandlerInterface');
$translation_handler->expects($this->once())
->method('getTranslationAccess')
->will($this->returnValue(AccessResult::allowed()));
$entity_manager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
$entity_manager->expects($this->once())
->method('getHandler')
->withAnyParameters()
->will($this->returnValue($translation_handler));
// Set our source and target languages.
$source = 'en';
$target = 'it';
// Set the mock language manager.
$language_manager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
$language_manager->expects($this->at(0))
->method('getLanguage')
->with($this->equalTo($source))
->will($this->returnValue(new Language(array('id' => 'en'))));
$language_manager->expects($this->at(1))
->method('getLanguages')
->will($this->returnValue(array('en' => array(), 'it' => array())));
$language_manager->expects($this->at(2))
->method('getLanguage')
->with($this->equalTo($source))
->will($this->returnValue(new Language(array('id' => 'en'))));
$language_manager->expects($this->at(3))
->method('getLanguage')
->with($this->equalTo($target))
->will($this->returnValue(new Language(array('id' => 'it'))));
// Set the mock entity. We need to use ContentEntityBase for mocking due to
// issues with phpunit and multiple interfaces.
$entity = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityBase')
->disableOriginalConstructor()
->getMock();
$entity->expects($this->once())
->method('getEntityTypeId');
$entity->expects($this->once())
->method('getTranslationLanguages')
->with()
->will($this->returnValue(array()));
$entity->expects($this->once())
->method('getCacheTags')
->will($this->returnValue(array('node:1337')));
// Set the route requirements.
$route = new Route('test_route');
$route->setRequirement('_access_content_translation_manage', 'create');
// Set up the route match.
$route_match = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
$route_match->expects($this->once())
->method('getParameter')
->with('node')
->will($this->returnValue($entity));
// Set the mock account.
$account = $this->getMock('Drupal\Core\Session\AccountInterface');
// The access check under test.
$check = new ContentTranslationManageAccessCheck($entity_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,73 @@
<?php
/**
* @file
* Contains \Drupal\Tests\content_translation\Unit\Menu\ContentTranslationLocalTasksTest.
*/
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 {
protected function setUp() {
$this->directoryList = array(
'content_translation' => 'core/modules/content_translation',
'node' => 'core/modules/node',
);
parent::setUp();
$entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
$entity_type->expects($this->any())
->method('getLinkTemplate')
->will($this->returnValueMap(array(
array('canonical', 'entity.node.canonical'),
array('drupal:content-translation-overview', 'entity.node.content_translation_overview'),
)));
$content_translation_manager = $this->getMock('Drupal\content_translation\ContentTranslationManagerInterface');
$content_translation_manager->expects($this->any())
->method('getSupportedEntityTypes')
->will($this->returnValue(array(
'node' => $entity_type,
)));
\Drupal::getContainer()->set('content_translation.manager', $content_translation_manager);
}
/**
* Tests the block admin display local tasks.
*
* @dataProvider providerTestBlockAdminDisplay
*/
public function testBlockAdminDisplay($route, $expected) {
$this->assertLocalTasks($route, $expected);
}
/**
* Provides a list of routes to test.
*/
public function providerTestBlockAdminDisplay() {
return array(
array('entity.node.canonical', array(array(
'content_translation.local_tasks:entity.node.content_translation_overview',
'entity.node.canonical',
'entity.node.edit_form',
'entity.node.delete_form',
'entity.node.version_history',
))),
array('entity.node.content_translation_overview', array(array(
'content_translation.local_tasks:entity.node.content_translation_overview',
'entity.node.canonical',
'entity.node.edit_form',
'entity.node.delete_form',
'entity.node.version_history',
))),
);
}
}