diff --git a/core/includes/entity.inc b/core/includes/entity.inc
index fd5a880..8cb009a 100644
--- a/core/includes/entity.inc
+++ b/core/includes/entity.inc
@@ -54,6 +54,21 @@ function entity_info_cache_clear() {
}
/**
+ * Returns the defined bundles for the given entity type.
+ *
+ * @param string $entity_type
+ * The entity type whose bundles should be returned.
+ *
+ * @return array
+ * An array containing the bundle names or the entity type name itself if no
+ * bundle is defined.
+ */
+function entity_get_bundles($entity_type) {
+ $entity_info = entity_get_info($entity_type);
+ return isset($entity_info['bundles']) ? array_keys($entity_info['bundles']) : array($entity_type);
+}
+
+/**
* Loads an entity from the database.
*
* @param string $entity_type
@@ -445,7 +460,7 @@ function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_st
// Invoke all specified builders for copying form values to entity properties.
if (isset($form['#entity_builders'])) {
foreach ($form['#entity_builders'] as $function) {
- $function($entity_type, $entity, $form, $form_state);
+ call_user_func_array($function, array($entity_type, $entity, &$form, &$form_state));
}
}
diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php
index 710792f..c6e041e 100644
--- a/core/lib/Drupal/Core/Entity/Entity.php
+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -275,37 +275,43 @@ public function getTranslation($langcode, $strict = TRUE) {
/**
* Returns the languages the entity is translated to.
*
- * @todo: Remove once all entity types implement the entity field API. This
- * is deprecated by
- * TranslatableInterface::getTranslationLanguages().
+ * @todo: Remove once all entity types implement the entity field API.
+ * This is deprecated by
+ * Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages().
*/
public function translations() {
- $languages = array();
+ return $this->getTranslationLanguages(FALSE);
+ }
+
+ /**
+ * Implements TranslatableInterface::getTranslationLanguages().
+ */
+ public function getTranslationLanguages($include_default = TRUE) {
+ // @todo: Replace by EntityNG implementation once all entity types have been
+ // converted to use the entity field API.
+ $default_language = $this->language();
+ $languages = array($default_language->langcode => $default_language);
$entity_info = $this->entityInfo();
- if ($entity_info['fieldable'] && ($default_language = $this->language())) {
+
+ if ($entity_info['fieldable']) {
// Go through translatable properties and determine all languages for
// which translated values are available.
foreach (field_info_instances($this->entityType, $this->bundle()) as $field_name => $instance) {
$field = field_info_field($field_name);
if (field_is_translatable($this->entityType, $field) && isset($this->$field_name)) {
- foreach ($this->$field_name as $langcode => $value) {
+ foreach (array_filter($this->$field_name) as $langcode => $value) {
$languages[$langcode] = TRUE;
}
}
}
- // Remove the default language from the translations.
+ $languages = array_intersect_key(language_list(LANGUAGE_ALL), $languages);
+ }
+
+ if (empty($include_default)) {
unset($languages[$default_language->langcode]);
- $languages = array_intersect_key(language_list(), $languages);
}
- return $languages;
- }
- /**
- * Implements TranslatableInterface::getTranslationLanguages().
- */
- public function getTranslationLanguages($include_default = TRUE) {
- // @todo: Replace by EntityNG implementation once all entity types have been
- // converted to use the entity field API.
+ return $languages;
}
/**
diff --git a/core/lib/Drupal/Core/Entity/EntityFormController.php b/core/lib/Drupal/Core/Entity/EntityFormController.php
index f75015f..2e9daac 100644
--- a/core/lib/Drupal/Core/Entity/EntityFormController.php
+++ b/core/lib/Drupal/Core/Entity/EntityFormController.php
@@ -178,6 +178,7 @@ public function validate(array $form, array &$form_state) {
* A reference to a keyed array containing the current state of the form.
*/
public function submit(array $form, array &$form_state) {
+ $this->submitEntityLanguage($form, $form_state);
$entity = $this->buildEntity($form, $form_state);
$this->setEntity($entity, $form_state);
return $entity;
@@ -212,7 +213,7 @@ public function delete(array $form, array &$form_state) {
*/
public function getFormLangcode(array $form_state) {
$entity = $this->getEntity($form_state);
- $translations = $entity->translations();
+ $translations = $entity->getTranslationLanguages();
if (!empty($form_state['langcode'])) {
$langcode = $form_state['langcode'];
@@ -234,6 +235,54 @@ public function getFormLangcode(array $form_state) {
}
/**
+ * Implements EntityFormControllerInterface::isDefaultFormLangcode().
+ */
+ public function isDefaultFormLangcode($form_state) {
+ return $this->getFormLangcode($form_state) == $this->getEntity($form_state)->language()->langcode;
+ }
+
+ /**
+ * Handle possible entity language changes.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param array $form_state
+ * A reference to a keyed array containing the current state of the form.
+ */
+ protected function submitEntityLanguage(array $form, array &$form_state) {
+ // Update the form language as it might have changed.
+ if (isset($form_state['values']['langcode']) && $this->isDefaultFormLangcode($form_state)) {
+ $form_state['langcode'] = $form_state['values']['langcode'];
+ }
+
+ $entity = $this->getEntity($form_state);
+ $entity_type = $entity->entityType();
+
+ if (field_has_translation_handler($entity_type)) {
+ $form_langcode = $this->getFormLangcode($form_state);
+
+ // If we are editing the default language values, we use the submitted
+ // entity language as the new language for fields to handle any language
+ // change. Otherwise the current form language is the proper value, since
+ // in this case it is not supposed to change.
+ $current_langcode = $entity->language()->langcode == $form_langcode ? $form_state['values']['langcode'] : $form_langcode;
+
+ foreach (field_info_instances($entity_type, $entity->bundle()) as $instance) {
+ $field_name = $instance['field_name'];
+ $field = field_info_field($field_name);
+ $previous_langcode = $form[$field_name]['#language'];
+
+ // Handle a possible language change: new language values are inserted,
+ // previous ones are deleted.
+ if ($field['translatable'] && $previous_langcode != $current_langcode) {
+ $form_state['values'][$field_name][$current_langcode] = $form_state['values'][$field_name][$previous_langcode];
+ $form_state['values'][$field_name][$previous_langcode] = array();
+ }
+ }
+ }
+ }
+
+ /**
* Implements Drupal\Core\Entity\EntityFormControllerInterface::buildEntity().
*/
public function buildEntity(array $form, array &$form_state) {
diff --git a/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php
index 26a227d..fecb078 100644
--- a/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php
@@ -44,6 +44,17 @@ public function build(array $form, array &$form_state, EntityInterface $entity);
public function getFormLangcode(array $form_state);
/**
+ * Checks whether the current form language matches the entity one.
+ *
+ * @param array $form_state
+ * A reference to a keyed array containing the current state of the form.
+ *
+ * @return boolean
+ * Returns TRUE if the entity form language matches the entity one.
+ */
+ public function isDefaultFormLangcode($form_state);
+
+ /**
* Returns the operation identifying the form controller.
*
* @return string
diff --git a/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php b/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php
index d06614d..812a192 100644
--- a/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php
+++ b/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php
@@ -64,14 +64,19 @@ public function buildEntity(array $form, array &$form_state) {
// without changing existing entity properties that are not being edited by
// this form. Copying field values must be done using field_attach_submit().
$values_excluding_fields = $info['fieldable'] ? array_diff_key($form_state['values'], field_info_instances($entity_type, $entity->bundle())) : $form_state['values'];
+ $translation = $entity->getTranslation($this->getFormLangcode($form_state), FALSE);
+ $definitions = $translation->getPropertyDefinitions();
foreach ($values_excluding_fields as $key => $value) {
- $entity->$key = $value;
+ if (isset($definitions[$key])) {
+ $translation->$key = $value;
+ }
}
- // Invoke all specified builders for copying form values to entity properties.
+ // Invoke all specified builders for copying form values to entity
+ // properties.
if (isset($form['#entity_builders'])) {
foreach ($form['#entity_builders'] as $function) {
- $function($entity_type, $entity, $form, $form_state);
+ call_user_func_array($function, array($entity_type, $entity, &$form, &$form_state));
}
}
diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php
index b8c38ce..29f0a36 100644
--- a/core/lib/Drupal/Core/Entity/EntityManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityManager.php
@@ -59,6 +59,10 @@
* Drupal\Core\Entity\EntityListController.
* - render_controller_class: The name of the class that is used to render the
* entities. Defaults to Drupal\Core\Entity\EntityRenderController.
+ * - translation_controller_class: (optional) The name of the translation
+ * controller class that should be used to handle the translation process.
+ * See Drupal\translation_entity\EntityTranslationControllerInterface for more
+ * information.
* - static_cache: (optional) Boolean indicating whether entities should be
* statically cached during a page request. Used by
* Drupal\Core\Entity\DatabaseStorageController. Defaults to TRUE.
@@ -140,6 +144,16 @@
* by default (e.g. right after the module exposing the view mode is
* enabled), but administrators can later use the Field UI to apply custom
* display settings specific to the view mode.
+ * - menu_base_path: (optional) The base menu router path to which the entity
+ * administration user interface responds. It can be used to generate UI
+ * links and to attach additional router items to the entity UI in a generic
+ * fashion.
+ * - menu_view_path: (optional) The menu router path to be used to view the
+ * entity.
+ * - menu_edit_path: (optional) The menu router path to be used to edit the
+ * entity.
+ * - menu_path_wildcard: (optional) A string identifying the menu loader in the
+ * router path.
*
* The defaults for the plugin definition are provided in
* \Drupal\Core\Entity\EntityManager::defaults.
diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php
index 2fc7764..35ef89d 100644
--- a/core/lib/Drupal/Core/Entity/EntityNG.php
+++ b/core/lib/Drupal/Core/Entity/EntityNG.php
@@ -269,21 +269,13 @@ public function getTranslationLanguages($include_default = TRUE) {
$translations[$this->language()->langcode] = TRUE;
}
- // Now get languages based upon translation langcodes.
- $languages = array_intersect_key(language_list(LANGUAGE_ALL), $translations);
+ // Now get languages based upon translation langcodes. Empty languages must
+ // be filtered out as they concern empty/unset properties.
+ $languages = array_intersect_key(language_list(LANGUAGE_ALL), array_filter($translations));
return $languages;
}
/**
- * Overrides Entity::translations().
- *
- * @todo: Remove once Entity::translations() gets removed.
- */
- public function translations() {
- return $this->getTranslationLanguages(FALSE);
- }
-
- /**
* Enables or disable the compatibility mode.
*
* @param bool $enabled
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index c174293..f8a9e30 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -1009,6 +1009,16 @@ function comment_links(Comment $comment, Node $node) {
$links['comment-forbidden']['html'] = TRUE;
}
}
+
+ // Add translations link for translation-enabled comment bundles.
+ if (module_exists('translation_entity') && translation_entity_translate_access($comment)) {
+ $links['comment-translations'] = array(
+ 'title' => t('translations'),
+ 'href' => 'comment/' . $comment->id() . '/translations',
+ 'html' => TRUE,
+ );
+ }
+
return $links;
}
@@ -1106,10 +1116,33 @@ function comment_form_node_type_form_alter(&$form, $form_state) {
DRUPAL_REQUIRED => t('Required'),
),
);
+ // @todo Remove this check once language settings are generalized.
+ if (module_exists('translation_entity')) {
+ $comment_form_state['translation_entity']['key'] = 'language_configuration';
+ $form['comment'] += translation_entity_enable_widget('comment', 'comment_node_' . $form['#node_type']->type, $comment_form, $comment_form_state);
+ array_unshift($form['#submit'], 'comment_translation_configuration_element_submit');
+ }
}
}
/**
+ * Comment translation settings submit handler.
+ */
+function comment_translation_configuration_element_submit($form, &$form_state) {
+ // The comment translation settings form element is embedded into the node
+ // type form. Hence we need to provide to the regular submit handler a
+ // manipulated form state to make it process comment settings instead of node
+ // settings.
+ $key = 'language_configuration';
+ $comment_form_state = array(
+ 'translation_entity' => array('key' => $key),
+ 'language' => array($key => array('entity_type' => 'comment', 'bundle' => 'comment_node_' . $form['#node_type']->type)),
+ 'values' => array($key => array('translation_entity' => $form_state['values']['translation_entity'])),
+ );
+ translation_entity_language_configuration_element_submit($form, $comment_form_state);
+}
+
+/**
* Implements hook_form_BASE_FORM_ID_alter().
*/
function comment_form_node_form_alter(&$form, $form_state) {
diff --git a/core/modules/comment/lib/Drupal/comment/CommentFormController.php b/core/modules/comment/lib/Drupal/comment/CommentFormController.php
index cead0cd..45ae3df 100644
--- a/core/modules/comment/lib/Drupal/comment/CommentFormController.php
+++ b/core/modules/comment/lib/Drupal/comment/CommentFormController.php
@@ -220,8 +220,6 @@ protected function actions(array $form, array &$form_state) {
),
);
- $element['#weight'] = $form['comment_body']['#weight'] + 0.01;
-
return $element;
}
diff --git a/core/modules/comment/lib/Drupal/comment/CommentTranslationController.php b/core/modules/comment/lib/Drupal/comment/CommentTranslationController.php
new file mode 100644
index 0000000..0cfa52b
--- /dev/null
+++ b/core/modules/comment/lib/Drupal/comment/CommentTranslationController.php
@@ -0,0 +1,26 @@
+ $entity->label()));
+ }
+
+}
diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php b/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php
index 2b00d45..e0bbfd9 100644
--- a/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php
+++ b/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php
@@ -24,6 +24,7 @@
* form_controller_class = {
* "default" = "Drupal\comment\CommentFormController"
* },
+ * translation_controller_class = "Drupal\comment\CommentTranslationController",
* base_table = "comment",
* uri_callback = "comment_uri",
* fieldable = TRUE,
diff --git a/core/modules/node/content_types.inc b/core/modules/node/content_types.inc
index a7faa59..a6839ae 100644
--- a/core/modules/node/content_types.inc
+++ b/core/modules/node/content_types.inc
@@ -224,6 +224,7 @@ function node_type_form($form, &$form_state, $type = NULL) {
),
'#default_value' => $language_configuration,
);
+
$form['#submit'][] = 'language_configuration_element_submit';
}
$form['display'] = array(
diff --git a/core/modules/node/lib/Drupal/node/NodeFormController.php b/core/modules/node/lib/Drupal/node/NodeFormController.php
index 5add3df..563a00f 100644
--- a/core/modules/node/lib/Drupal/node/NodeFormController.php
+++ b/core/modules/node/lib/Drupal/node/NodeFormController.php
@@ -317,8 +317,6 @@ public function validate(array $form, array &$form_state) {
* Overrides Drupal\Core\Entity\EntityFormController::submit().
*/
public function submit(array $form, array &$form_state) {
- $this->submitNodeLanguage($form, $form_state);
-
// Build the node object from the submitted values.
$node = parent::submit($form, $form_state);
@@ -337,36 +335,6 @@ public function submit(array $form, array &$form_state) {
}
/**
- * Handle possible node language changes.
- */
- protected function submitNodeLanguage(array $form, array &$form_state) {
- if (field_has_translation_handler('node', 'node')) {
- $bundle = $form_state['values']['type'];
- $entity = $this->getEntity($form_state);
- $form_langcode = $this->getFormLangcode($form_state);
-
- // If we are editing the default language values, we use the submitted
- // entity language as the new language for fields to handle any language
- // change. Otherwise the current form language is the proper value, since
- // in this case it is not supposed to change.
- $current_langcode = $entity->language()->langcode == $form_langcode ? $form_state['values']['langcode'] : $form_langcode;
-
- foreach (field_info_instances('node', $bundle) as $instance) {
- $field_name = $instance['field_name'];
- $field = field_info_field($field_name);
- $previous_langcode = $form[$field_name]['#language'];
-
- // Handle a possible language change: new language values are inserted,
- // previous ones are deleted.
- if ($field['translatable'] && $previous_langcode != $current_langcode) {
- $form_state['values'][$field_name][$current_langcode] = $form_state['values'][$field_name][$previous_langcode];
- $form_state['values'][$field_name][$previous_langcode] = array();
- }
- }
- }
- }
-
- /**
* Form submission handler for the 'preview' action.
*
* @param $form
diff --git a/core/modules/node/lib/Drupal/node/NodeTranslationController.php b/core/modules/node/lib/Drupal/node/NodeTranslationController.php
new file mode 100644
index 0000000..da078ac
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/NodeTranslationController.php
@@ -0,0 +1,50 @@
+ 'additional_settings',
+ '#weight' => 100,
+ '#attributes' => array(
+ 'class' => array('node-translation-options'),
+ ),
+ );
+ }
+ }
+
+ /**
+ * Overrides EntityTranslationController::entityFormTitle().
+ */
+ protected function entityFormTitle(EntityInterface $entity) {
+ $type_name = node_get_type_label($entity);
+ return t('Edit @type @title', array('@type' => $type_name, '@title' => $entity->label()));
+ }
+}
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
index f161d81..a9139e9 100644
--- a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
+++ b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
@@ -24,6 +24,7 @@
* form_controller_class = {
* "default" = "Drupal\node\NodeFormController"
* },
+ * translation_controller_class = "Drupal\node\NodeTranslationController",
* base_table = "node",
* revision_table = "node_revision",
* uri_callback = "node_uri",
diff --git a/core/modules/node/node.js b/core/modules/node/node.js
index 0899d3c..e54a4d4 100644
--- a/core/modules/node/node.js
+++ b/core/modules/node/node.js
@@ -42,6 +42,21 @@ Drupal.behaviors.nodeFieldsetSummaries = {
}
return vals.join(', ');
});
+
+ $context.find('fieldset.node-translation-options').drupalSetSummary(function (context) {
+ var translate;
+ var $checkbox = $context.find('.form-item-translation-translate input');
+
+ if ($checkbox.size()) {
+ translate = $checkbox.is(':checked') ? Drupal.t('Needs to be updated') : Drupal.t('Does not need to be updated');
+ }
+ else {
+ $checkbox = $context.find('.form-item-translation-retranslate input');
+ translate = $checkbox.is(':checked') ? Drupal.t('Flag other translations as outdated') : Drupal.t('Do not flag other translations as outdated');
+ }
+
+ return translate;
+ });
}
};
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index e44f43c..f43c07e 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -264,6 +264,8 @@ function node_admin_paths() {
'node/*/revisions' => TRUE,
'node/*/revisions/*/revert' => TRUE,
'node/*/revisions/*/delete' => TRUE,
+ 'node/*/translations' => TRUE,
+ 'node/*/translations/*' => TRUE,
'node/add' => TRUE,
'node/add/*' => TRUE,
);
@@ -2449,7 +2451,7 @@ function node_update_index() {
$counter = 0;
foreach (node_load_multiple($nids) as $node) {
// Determine when the maximum number of indexable items is reached.
- $counter += 1 + count($node->translations());
+ $counter += count($node->getTranslationLanguages());
if ($counter > $limit) {
break;
}
@@ -2469,7 +2471,7 @@ function _node_index_node(Node $node) {
// results half-life calculation.
variable_set('node_cron_last', $node->changed);
- $languages = array_merge(array(language_load($node->langcode)), $node->translations());
+ $languages = $node->getTranslationLanguages();
foreach ($languages as $language) {
// Render the node.
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index 9ee32ec..b5a4f94 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -5,6 +5,9 @@
* Test module for the entity API providing an entity type for testing.
*/
+use Drupal\entity_test\Plugin\Core\Entity\EntityTest;
+
+
/**
* Implements hook_entity_info_alter().
*/
@@ -69,8 +72,8 @@ function entity_test_add() {
/**
* Menu callback: displays the 'Edit existing entity_test' form.
*/
-function entity_test_edit($entity) {
- drupal_set_title(t('entity_test @id', array('@id' => $entity->id())), PASS_THROUGH);
+function entity_test_edit(EntityTest $entity) {
+ drupal_set_title($entity->label(), PASS_THROUGH);
return entity_get_form($entity);
}
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php
index e51f474..a65f06f 100644
--- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php
@@ -43,21 +43,14 @@ public function form(array $form, array &$form_state, EntityInterface $entity) {
'#weight' => -10,
);
- return $form;
- }
+ $form['langcode'] = array(
+ '#title' => t('Language'),
+ '#type' => 'language_select',
+ '#default_value' => $entity->language()->langcode,
+ '#languages' => LANGUAGE_ALL,
+ );
- /**
- * Overrides Drupal\Core\Entity\EntityFormController::submit().
- */
- public function submit(array $form, array &$form_state) {
- $entity = parent::submit($form, $form_state);
- $langcode = $this->getFormLangcode($form_state);
- // Updates multilingual properties.
- $translation = $entity->getTranslation($langcode);
- foreach (array('name', 'user_id') as $name) {
- $translation->$name->setValue($form_state['values'][$name]);
- }
- return $entity;
+ return $form;
}
/**
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php
index fbb735d..da502b8 100644
--- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php
@@ -84,6 +84,13 @@ protected function attachPropertyData(&$queried_entities) {
protected function postSave(EntityInterface $entity, $update) {
$default_langcode = $entity->language()->langcode;
+ // Delete and insert to handle removed values.
+ db_delete('entity_test_property_data')
+ ->condition('id', $entity->id())
+ ->execute();
+
+ $query = db_insert('entity_test_property_data');
+
foreach ($entity->getTranslationLanguages() as $langcode => $language) {
$translation = $entity->getTranslation($langcode);
@@ -95,12 +102,12 @@ protected function postSave(EntityInterface $entity, $update) {
'user_id' => $translation->user_id->value,
);
- db_merge('entity_test_property_data')
- ->fields($values)
- ->condition('id', $values['id'])
- ->condition('langcode', $values['langcode'])
- ->execute();
+ $query
+ ->fields(array_keys($values))
+ ->values($values);
}
+
+ $query->execute();
}
/**
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestTranslationController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestTranslationController.php
new file mode 100644
index 0000000..e7ca050
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestTranslationController.php
@@ -0,0 +1,28 @@
+getTranslation($langcode);
+ foreach ($translation->getPropertyDefinitions() as $property_name => $langcode) {
+ $translation->$property_name = array();
+ }
+ }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php
index 240c5c7..3c948bd 100644
--- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php
@@ -22,13 +22,15 @@
* form_controller_class = {
* "default" = "Drupal\entity_test\EntityTestFormController"
* },
+ * translation_controller_class = "Drupal\entity_test\EntityTestTranslationController",
* base_table = "entity_test",
* data_table = "entity_test_property_data",
* fieldable = TRUE,
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid"
- * }
+ * },
+ * menu_base_path = "entity-test/manage/%entity_test"
* )
*/
class EntityTest extends EntityNG {
@@ -74,4 +76,12 @@ public function __construct(array $values, $entity_type) {
unset($this->name);
unset($this->user_id);
}
+
+ /**
+ * Overrides Drupal\entity\Entity::label().
+ */
+ public function label($langcode = LANGUAGE_DEFAULT) {
+ return $this->getTranslation($langcode)->name->value;
+ }
+
}
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php
index 8aedeee..51468af 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php
@@ -24,6 +24,7 @@
* form_controller_class = {
* "default" = "Drupal\taxonomy\TermFormController"
* },
+ * translation_controller_class = "Drupal\taxonomy\TermTranslationController",
* base_table = "taxonomy_term_data",
* uri_callback = "taxonomy_term_uri",
* fieldable = TRUE,
@@ -41,7 +42,8 @@
* "label" = "Taxonomy term page",
* "custom_settings" = FALSE
* }
- * }
+ * },
+ * menu_base_path = "taxonomy/term/%taxonomy_term"
* )
*/
class Term extends Entity implements ContentEntityInterface {
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TermTranslationController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TermTranslationController.php
new file mode 100644
index 0000000..a8e6dd7
--- /dev/null
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermTranslationController.php
@@ -0,0 +1,38 @@
+getSourceLangcode($form_state)) {
+ $entity = translation_entity_form_controller($form_state)->getEntity($form_state);
+ // We need a redirect here, otherwise we would get an access denied page
+ // since the curret URL would be preserved and we would try to add a
+ // translation for a language that already has a translation.
+ $form_state['redirect'] = $this->getEditPath($entity);
+ }
+ }
+}
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php
index f6e5110..c26251e 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php
@@ -98,6 +98,12 @@ protected function actions(array $form, array &$form_state) {
array_unshift($actions['submit']['#submit'],'language_configuration_element_submit');
array_unshift($actions['submit']['#submit'], array($this, 'languageConfigurationSubmit'));
}
+ // We cannot leverage the regular submit handler definition because we
+ // have button-specific ones here. Hence we need to explicitly set it for
+ // the submit action, otherwise it would be ignored.
+ if (module_exists('translation_entity')) {
+ array_unshift($actions['submit']['#submit'], 'translation_entity_language_configuration_element_submit');
+ }
return $actions;
}
else {
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index c92ce64..75573a9 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -357,6 +357,8 @@ function taxonomy_admin_paths() {
$paths = array(
'taxonomy/term/*/edit' => TRUE,
'taxonomy/term/*/delete' => TRUE,
+ 'taxonomy/term/*/translations' => TRUE,
+ 'taxonomy/term/*/translations/*' => TRUE,
);
return $paths;
}
diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationController.php b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationController.php
new file mode 100644
index 0000000..ddf6589
--- /dev/null
+++ b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationController.php
@@ -0,0 +1,400 @@
+entityType = $entity_type;
+ $this->entityInfo = $entity_info;
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::removeTranslation().
+ */
+ public function removeTranslation(EntityInterface $entity, $langcode) {
+ $translations = $entity->getTranslationLanguages();
+ // @todo Handle properties.
+ // Remove field translations.
+ foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $instance) {
+ $field_name = $instance['field_name'];
+ $field = field_info_field($field_name);
+ if ($field['translatable']) {
+ $entity->{$field_name}[$langcode] = array();
+ }
+ }
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::retranslate().
+ */
+ public function retranslate(EntityInterface $entity, $langcode = NULL) {
+ $updated_langcode = !empty($langcode) ? $langcode : $entity->language()->langcode;
+ $translations = $entity->getTranslationLanguages();
+ foreach ($translations as $langcode => $language) {
+ $entity->retranslate[$langcode] = $langcode != $updated_langcode;
+ }
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::getBasePath().
+ */
+ public function getBasePath(EntityInterface $entity) {
+ return $this->getPathInstance($this->entityInfo['menu_base_path'], $entity->id());
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::getEditPath().
+ */
+ public function getEditPath(EntityInterface $entity) {
+ return isset($this->entityInfo['menu_edit_path']) ? $this->getPathInstance($this->entityInfo['menu_edit_path'], $entity->id()) : FALSE;
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::getViewPath().
+ */
+ public function getViewPath(EntityInterface $entity) {
+ return isset($this->entityInfo['menu_view_path']) ? $this->getPathInstance($this->entityInfo['menu_view_path'], $entity->id()) : FALSE;
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::getAccess().
+ */
+ public function getAccess(EntityInterface $entity, $op) {
+ return TRUE;
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::getTranslationAccess().
+ */
+ public function getTranslationAccess(EntityInterface $entity, $langcode) {
+ $entity_type = $entity->entityType();
+ return (user_access('translate any entity') || user_access("translate $entity_type entities")) && ($langcode != $entity->language()->langcode || user_access('edit original values'));
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::getSourceLanguage().
+ */
+ public function getSourceLangcode(array $form_state) {
+ return isset($form_state['translation_entity']['source']) ? $form_state['translation_entity']['source']->langcode : FALSE;
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::entityFormAlter().
+ */
+ public function entityFormAlter(array &$form, array &$form_state, EntityInterface $entity) {
+ $form_controller = translation_entity_form_controller($form_state);
+ $form_langcode = $form_controller->getFormLangcode($form_state);
+ $entity_langcode = $entity->language()->langcode;
+ $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_controller->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 = language_list();
+ 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->language()->langcode) {
+ $t_args = array('%language' => $languages[$form_langcode]->name, '%title' => $entity->label());
+ $title = empty($source_langcode) ? $title . ' [' . t('%language translation', $t_args) . ']' : t('Create %language translation of %title', $t_args);
+ }
+ drupal_set_title($title, PASS_THROUGH);
+ }
+
+ // 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' => 'fieldset',
+ '#title' => t('Source language: @language', array('@language' => $languages[$source_langcode]->name)),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#tree' => TRUE,
+ '#weight' => -100,
+ 'source' => array(
+ '#type' => 'select',
+ '#default_value' => $source_langcode,
+ '#options' => array(),
+ ),
+ 'submit' => array(
+ '#type' => 'submit',
+ '#value' => t('Change'),
+ '#submit' => array(array($this, 'entityFormSourceChange')),
+ ),
+ );
+ foreach (language_list(LANGUAGE_CONFIGURABLE) as $language) {
+ if (isset($translations[$language->langcode])) {
+ $form['source_langcode']['source']['#options'][$language->langcode] = $language->name;
+ }
+ }
+ }
+
+ // Disable languages for existing translations, so it is not possible to
+ // switch this node to some language which is already in the translation
+ // set.
+ $language_widget = isset($form['langcode']) && $form['langcode']['#type'] == 'language_select';
+ if ($language_widget && $has_translations) {
+ $form['langcode']['#options'] = array();
+ foreach (language_list(LANGUAGE_CONFIGURABLE) as $language) {
+ if (empty($translations[$language->langcode]) || $language->langcode == $entity_langcode) {
+ $form['langcode']['#options'][$language->langcode] = $language->name;
+ }
+ }
+ }
+
+ if ($is_translation) {
+ if ($language_widget) {
+ $form['langcode']['#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;
+ }
+ }
+ $form['actions']['delete_translation'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete translation'),
+ '#weight' => $weight,
+ '#submit' => array(array($this, 'entityFormDeleteTranslation')),
+ );
+ }
+
+ // 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['translation'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Translation'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#tree' => TRUE,
+ '#weight' => 10,
+ '#access' => $this->getTranslationAccess($entity, $form_langcode),
+ '#multilingual' => TRUE,
+ );
+
+ $translate = !$new_translation && $entity->retranslate[$form_langcode];
+ if (!$translate) {
+ $form['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['translation']['translate'] = 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.'),
+ );
+ }
+
+ if ($language_widget) {
+ $form_langcode['#multilingual'] = TRUE;
+ }
+
+ $form['#process'][] = array($this, 'entityFormSharedElements');
+ }
+
+ // Process the submitted values before they are stored.
+ $form['#entity_builders'][] = array($this, 'entityFormEntityBuild');
+
+ // Handle entity deletion.
+ if (isset($form['actions']['delete'])) {
+ $form['actions']['delete']['#submit'][] = array($this, 'entityFormDelete');
+ }
+ }
+
+ /**
+ * Process callback that handles entity form shared elements.
+ */
+ public function entityFormSharedElements($element) {
+ 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'));
+ }
+
+ foreach (element_children($element) as $key) {
+ if (!isset($element[$key]['#type'])) {
+ $this->entityFormSharedElements($element[$key]);
+ }
+ 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'])) {
+ $this->addTranslatabilityClue($element[$key]);
+ }
+ }
+ }
+
+ 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 = ' (' . t('all languages') . ')';
+ $fapi_title_elements = array_flip(array('checkbox', 'checkboxes', 'date', '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.
+ */
+ public function entityFormEntityBuild($entity_type, $entity, $form, &$form_state) {
+ $form_controller = translation_entity_form_controller($form_state);
+ $form_langcode = $form_controller->getFormLangcode($form_state);
+ $source_langcode = $this->getSourceLangcode($form_state);
+
+ if ($source_langcode) {
+ // @todo Use the entity setter when all entities support multilingual
+ // properties.
+ $entity->source[$form_langcode] = $source_langcode;
+ }
+
+ // Ensure every key has at least a default value. Subclasses may provide
+ // entity-specific values to alter them.
+ $values = isset($form_state['values']['translation']) ? $form_state['values']['translation'] : array();
+ $entity->retranslate[$form_langcode] = isset($values['translate']) && $values['translate'];
+
+ if (!empty($values['retranslate'])) {
+ $this->retranslate($entity, $form_langcode);
+ }
+ }
+
+ /**
+ * Submit handler for the source language change.
+ */
+ public function entityFormSourceChange($form, &$form_state) {
+ $form_controller = translation_entity_form_controller($form_state);
+ $entity = $form_controller->getEntity($form_state);
+ $source = $form_state['values']['source_langcode']['source'];
+ $path = $this->getBasePath($entity) . '/translations/add/' . $source . '/' . $form_controller->getFormLangcode($form_state);
+ $form_state['redirect'] = array('path' => $path);
+ $languages = language_list();
+ drupal_set_message(t('Source language set to: %language', array('%language' => $languages[$source]->name)));
+ }
+
+ /**
+ * Submit handler for the entity deletion.
+ */
+ function entityFormDelete($form, &$form_state) {
+ $form_controller = translation_entity_form_controller($form_state);
+ $entity = $form_controller->getEntity($form_state);
+ if (count($entity->getTranslationLanguages()) > 1) {
+ drupal_set_message(t('This will delete all the translations of %label.', array('%label' => $entity->label())), 'warning');
+ }
+ }
+
+ /**
+ * Submit handler for the entity translation deletion.
+ */
+ function entityFormDeleteTranslation($form, &$form_state) {
+ $form_controller = translation_entity_form_controller($form_state);
+ $entity = $form_controller->getEntity($form_state);
+ $base_path = $this->getBasePath($entity);
+ $form_langcode = $form_controller->getFormLangcode($form_state);
+ $form_state['redirect'] = $base_path . '/translations/delete/' . $form_langcode;
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * Returns an instance of the given path.
+ *
+ * @param $path
+ * An internal path containing the entity id wildcard.
+ *
+ * @return string
+ * The instantiated path.
+ */
+ protected function getPathInstance($path, $entity_id) {
+ $wildcard = $this->entityInfo['menu_path_wildcard'];
+ return str_replace($wildcard, $entity_id, $path);
+ }
+}
diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationControllerInterface.php b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationControllerInterface.php
new file mode 100644
index 0000000..7a37a2a
--- /dev/null
+++ b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationControllerInterface.php
@@ -0,0 +1,188 @@
+ 'mymodule/myentity/%my_entity_loader',
+ * 'menu_path_widlcard' => '%my_entity_loader',
+ * 'translation_controller_class' => 'Drupal\mymodule\MyEntityTranslationController',
+ * 'translation' => array(
+ * 'translation_entity' => array(
+ * 'access_callback' => 'mymodule_myentity_translate_access',
+ * 'access_arguments' => array(2),
+ * ),
+ * ),
+ * );
+ * }
+ * @endcode
+ */
+interface EntityTranslationControllerInterface {
+
+ /**
+ * Returns the base path for the current entity.
+ *
+ * @param Drupal\Core\Entity\EntityInterface $entity
+ * The entity to the path should refer to.
+ *
+ * @return string
+ * The entity base path.
+ */
+ public function getBasePath(EntityInterface $entity);
+
+ /**
+ * Returns the path of the entity edit form.
+ *
+ * @param Drupal\Core\Entity\EntityInterface $entity
+ * The entity to the path should refer to.
+ *
+ * @return string
+ * The entity edit path.
+ */
+ public function getEditPath(EntityInterface $entity);
+
+ /**
+ * Returns the path of the entity view page.
+ *
+ * @param Drupal\Core\Entity\EntityInterface $entity
+ * The entity to the path should refer to.
+ *
+ * @return string
+ * The entity view path.
+ */
+ public function getViewPath(EntityInterface $entity);
+
+ /**
+ * Checks if the user can perform the given operation on the wrapped entity.
+ *
+ * @param Drupal\Core\Entity\EntityInterface $entity
+ * The entity access should be checked for.
+ * @param string $op
+ * The operation to be performed. Possible values are:
+ * - "view"
+ * - "update"
+ * - "delete"
+ * - "create"
+ *
+ * @return
+ * TRUE if the user is allowed to perform the given operation, FALSE
+ * otherwise.
+ */
+ public function getAccess(EntityInterface $entity, $op);
+
+ /**
+ * Checks if a user is allowed to edit the given translation.
+ *
+ * @param Drupal\Core\Entity\EntityInterface $entity
+ * The entity whose translation has to be accessed.
+ * @param string $langcode
+ * The language code identifying the translation to be accessed.
+ *
+ * @return boolean
+ * TRUE if the operation may be performed, FALSE otherwise.
+ */
+ public function getTranslationAccess(EntityInterface $entity, $langcode);
+
+ /**
+ * Retrieves the source language for the translation being created.
+ *
+ * @param array $form_state
+ * The form state array.
+ *
+ * @return string
+ * The source language code.
+ */
+ public function getSourceLangcode(array $form_state);
+
+ /**
+ * Removes the translation values from the given entity.
+ *
+ * @param Drupal\Core\Entity\EntityInterface $entity
+ * The entity whose values should be removed.
+ * @param string $langcode
+ * The language code identifying the translation being deleted.
+ */
+ public function removeTranslation(EntityInterface $entity, $langcode);
+
+ /**
+ * 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 array $form_state
+ * The form state array.
+ * @param Drupal\Core\Entity\EntityInterface $entity
+ * The entity being created or edited.
+ */
+ public function entityFormAlter(array &$form, array &$form_state, EntityInterface $entity);
+}
diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationUITest.php b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationUITest.php
new file mode 100644
index 0000000..a0876a4
--- /dev/null
+++ b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationUITest.php
@@ -0,0 +1,191 @@
+ 'Translation UI',
+ 'description' => 'Tests the basic entity translation UI.',
+ 'group' => 'Entity Translation UI',
+ );
+ }
+
+ function setUp() {
+ parent::setUp();
+
+ $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages'));
+ $this->drupalLogin($admin_user);
+
+ $languages = array('it' => 'Italian', 'fr' => 'French');
+ $this->langcodes = array_keys($languages);
+ array_unshift($this->langcodes, language_default()->langcode);
+
+ // Add predefined language.
+ foreach ($languages as $langcode => $name) {
+ $edit = array('predefined_langcode' => $langcode);
+ $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
+ $this->assertText($name, 'Language added successfully.');
+ }
+
+ // Enable translation for the entity_test entity type and ensure the change
+ // is picked up.
+ translation_entity_set_config('entity_test', 'entity_test', 'enabled', TRUE);
+ drupal_static_reset();
+ entity_info_cache_clear();
+ menu_router_rebuild();
+
+ $translator = $this->drupalCreateUser(array('administer entity_test content', 'translate entity_test entities', 'edit original values'));
+ $this->drupalLogin($translator);
+ }
+
+ /**
+ * Tests the basic translation UI.
+ */
+ function testTranslationUI() {
+ // Make the test field translatable.
+ $field = field_info_field('field_test_text');
+ $field['translatable'] = TRUE;
+ field_update_field($field);
+
+ // Create a new test entity with original values in the default language.
+ $default_langcode = language_default()->langcode;
+
+ $values[$default_langcode] = array(
+ 'langcode' => $default_langcode,
+ 'name' => $this->randomName(8),
+ 'user_id' => mt_rand(1, 128),
+ 'field_test_text' => $this->randomName(16),
+ );
+
+ $this->drupalPost('entity-test/add', $this->getEditValues($values, $default_langcode, TRUE), t('Save'));
+ $entity = $this->loadEntityByName($values[$default_langcode]['name']);
+ $this->assertTrue($entity, t('Entity found in the database.'));
+
+ $translation = $entity->getTranslation($default_langcode);
+ foreach ($values[$default_langcode] as $property => $value) {
+ if ($property != 'langcode') {
+ // @todo Remove this workaround when field default language is correctly
+ // handled.
+ $stored_value = $property != 'field_test_text' ? $translation->get($property)->value : $entity->values['field_test_text'][$default_langcode][0]['value'];
+ $message = format_string('@property correctly stored in the default language.', array('@property' => $property));
+ $this->assertEqual($stored_value, $value, $message);
+ }
+ }
+
+ // Add an entity translation.
+ $langcode = 'it';
+ $values[$langcode] = array(
+ 'name' => $this->randomName(8),
+ 'user_id' => mt_rand(1, 128),
+ 'field_test_text' => $this->randomName(16),
+ );
+
+ $this->drupalPost($langcode . '/entity-test/manage/' . $entity->id() . '/translations/add/' . $default_langcode . '/' . $langcode, $this->getEditValues($values, $langcode), t('Save'));
+ $this->assertNoFieldByXPath('//select[@id="edit-langcode"]', NULL, 'Language selector correclty disabled on translations.');
+ $this->assertNoRaw('all languages', t('All form elements are translatable.'));
+ $entity = entity_test_load($entity->id(), TRUE);
+
+ // Switch the source language.
+ $langcode = 'fr';
+ $source_langcode = 'it';
+ $edit = array('source_langcode[source]' => $source_langcode);
+ $this->drupalPost($langcode . '/entity-test/manage/' . $entity->id() . '/translations/add/' . $default_langcode . '/' . $langcode, $edit, t('Change'));
+ $this->assertFieldByXPath('//input[@name="field_test_text[fr][0][value]"]', $values[$source_langcode]['field_test_text'], 'Source language correctly switched.');
+
+ // Add another translation and mark the other ones as outdated.
+ $values[$langcode] = array(
+ 'name' => $this->randomName(8),
+ 'user_id' => mt_rand(1, 128),
+ 'field_test_text' => $this->randomName(16),
+ );
+ $edit = $this->getEditValues($values, $langcode) + array('translation[retranslate]' => TRUE);
+ $this->drupalPost(NULL, $edit, t('Save'));
+ $entity = entity_test_load($entity->id(), TRUE);
+
+ // Check that the entered values have been correctly stored.
+ foreach ($values as $langcode => $property_values) {
+ $translation = $entity->getTranslation($langcode);
+ foreach ($property_values as $property => $value) {
+ if ($property != 'langcode') {
+ $stored_value = $translation->get($property);
+ // @todo Remove this workaround when field default language is correctly
+ // handled.
+ $stored_value = $property != 'field_test_text' ? $translation->get($property)->value : $entity->values['field_test_text'][$langcode][0]['value'];
+ $message = format_string('%property correctly stored with language %language.', array('%property' => $property, '%language' => $langcode));
+ $this->assertEqual($stored_value, $value, $message);
+ }
+ }
+ }
+
+ // Check that every translation has the correct "outdated" status.
+ foreach ($this->langcodes as $enabled_langcode) {
+ $prefix = $enabled_langcode != $default_langcode ? $enabled_langcode . '/' : '';
+ $this->drupalGet($prefix . 'entity-test/manage/' . $entity->id() . '/edit');
+ if ($enabled_langcode == $langcode) {
+ $this->assertFieldByXPath('//input[@name="translation[retranslate]"]', FALSE, 'The retranslate flag is not checked by default.');
+ }
+ else {
+ $this->assertFieldByXPath('//input[@name="translation[translate]"]', TRUE, 'The translate flag is checked by default.');
+ $edit = array('translation[translate]' => FALSE);
+ $this->drupalPost(NULL, $edit, t('Save'));
+ $this->assertFieldByXPath('//input[@name="translation[retranslate]"]', FALSE, 'The retranslate flag is now shown.');
+ $entity = entity_test_load($entity->id(), TRUE);
+ $this->assertFalse($entity->retranslate[$enabled_langcode], 'The "outdated" status has been correctly stored.');
+ }
+ }
+
+ // Confirm and delete a translation.
+ $this->drupalPost(NULL, array(), t('Delete translation'));
+ $this->drupalPost(NULL, array(), t('Delete'));
+ $entity = entity_test_load($entity->id(), TRUE);
+ $translations = $entity->getTranslationLanguages();
+ $this->assertTrue(count($translations) == 2 && empty($translations[$enabled_langcode]), 'Translation successfully deleted.');
+ }
+
+ /**
+ * Returns an edit array containing the values to be posted.
+ */
+ protected function getEditValues($values, $langcode, $new = FALSE) {
+ $edit = $values[$langcode];
+ $langcode = $new ? LANGUAGE_NOT_SPECIFIED : $langcode;
+ $edit["field_test_text[$langcode][0][value]"] = $edit['field_test_text'];
+ unset($edit['field_test_text']);
+ return $edit;
+ }
+
+ /**
+ * Loads a test entity by name.
+ *
+ * @return Drupal\entity_test\EntityTest
+ * A test entity matching the given name.
+ */
+ protected function loadEntityByName($name) {
+ return current(entity_load_multiple_by_properties('entity_test', array('name' => $name)));
+ }
+}
diff --git a/core/modules/translation_entity/translation_entity.admin.inc b/core/modules/translation_entity/translation_entity.admin.inc
new file mode 100644
index 0000000..b4213e4
--- /dev/null
+++ b/core/modules/translation_entity/translation_entity.admin.inc
@@ -0,0 +1,252 @@
+ $field_name);
+
+ $warning = t('By submitting this form these changes will apply to the %name field everywhere it is used.', $t_args);
+ if ($field['translatable']) {
+ $title = t('Are you sure you want to disable translation for the %name field?', $t_args);
+ $warning .= "
" . t("All the existing translations of this field will be deleted.
This action cannot be undone.");
+ }
+ else {
+ $title = t('Are you sure you want to enable translation for the %name field?', $t_args);
+ }
+
+ // We need to keep some information for later processing.
+ $form_state['field'] = $field;
+
+ // Store the 'translatable' status on the client side to prevent outdated form
+ // submits from toggling translatability.
+ $form['translatable'] = array(
+ '#type' => 'hidden',
+ '#default_value' => $field['translatable'],
+ );
+
+ return confirm_form($form, $title, '', $warning);
+}
+
+/**
+ * Submit handler for the field settings form.
+ *
+ * This submit handler maintains consistency between the translatability of an
+ * entity and the language under which the field data is stored. When a field is
+ * marked as translatable, all the data in
+ * $entity->{field_name}[LANGUAGE_NOT_SPECIFIED] is moved to
+ * $entity->{field_name}[$entity_language]. When a field is marked as
+ * untranslatable the opposite process occurs. Note that marking a field as
+ * untranslatable will cause all of its translations to be permanently removed,
+ * with the exception of the one corresponding to the entity language.
+ */
+function translation_entity_translatable_form_submit(array $form, array $form_state) {
+ // This is the current state that we want to reverse.
+ $translatable = $form_state['values']['translatable'];
+ $field_name = $form_state['field']['field_name'];
+ $field = field_info_field($field_name);
+
+ if ($field['translatable'] !== $translatable) {
+ // Field translatability has changed since form creation, abort.
+ $t_args = array('%field_name');
+ $msg = $translatable ?
+ t('The field %field_name is already translatable. No change was performed.', $t_args):
+ t('The field %field_name is already untranslatable. No change was performed.', $t_args);
+ drupal_set_message($msg, 'warning');
+ return;
+ }
+
+ // If a field is untranslatable, it can have no data except under
+ // LANGUAGE_NOT_SPECIFIED. Thus we need a field to be translatable before we convert
+ // data to the entity language. Conversely we need to switch data back to
+ // LANGUAGE_NOT_SPECIFIED before making a field untranslatable lest we lose
+ // information.
+ $operations = array(
+ array('translation_entity_translatable_batch', array(!$translatable, $field_name)),
+ array('translation_entity_translatable_switch', array(!$translatable, $field_name)),
+ );
+ $operations = $translatable ? $operations : array_reverse($operations);
+
+ $t_args = array('%field' => $field_name);
+ $title = !$translatable ? t('Enabling translation for the %field field', $t_args) : t('Disabling translation for the %field field', $t_args);
+
+ $batch = array(
+ 'title' => $title,
+ 'operations' => $operations,
+ 'finished' => 'translation_entity_translatable_batch_done',
+ 'file' => drupal_get_path('module', 'translation_entity') . '/translation_entity.admin.inc',
+ );
+
+ batch_set($batch);
+}
+
+/*
+ * Toggles translatability of the given field.
+ *
+ * This is called from a batch operation, but should only run once per field.
+ */
+function translation_entity_translatable_switch($translatable, $field_name) {
+ $field = field_info_field($field_name);
+
+ if ($field['translatable'] === $translatable) {
+ return;
+ }
+
+ $field['translatable'] = $translatable;
+ field_update_field($field);
+}
+
+/**
+ * Converts field data to or from LANGUAGE_NOT_SPECIFIED.
+ */
+function translation_entity_translatable_batch($translatable, $field_name, &$context) {
+ $entity_types = array();
+
+ // Determine the entity types to act on.
+ foreach (field_info_instances() as $entity_type => $info) {
+ foreach ($info as $bundle => $instances) {
+ foreach ($instances as $instance_field_name => $instance) {
+ if ($instance_field_name == $field_name) {
+ $entity_types[] = $entity_type;
+ break 2;
+ }
+ }
+ }
+ }
+
+ if (empty($context['sandbox'])) {
+ $context['sandbox']['progress'] = 0;
+ $context['sandbox']['max'] = 0;
+
+ foreach ($entity_types as $entity_type) {
+ // How many entities will need processing?
+ $query = entity_query($entity_type);
+ $count = $query
+ ->exists($field_name)
+ ->count()
+ ->execute();
+
+ $context['sandbox']['progress_entity_type'][$entity_type] = 0;
+ $context['sandbox']['max_entity_type'][$entity_type] = $count;
+ }
+
+ // @todo Move this into the loop above as soon as entity query correctly
+ // handles count queries on fields shared among different entities.
+ $context['sandbox']['max'] += $count;
+
+ if ($context['sandbox']['max'] === 0) {
+ // Nothing to do.
+ $context['finished'] = 1;
+ return;
+ }
+ }
+
+ foreach ($entity_types as $entity_type) {
+ if ($context['sandbox']['max_entity_type'][$entity_type] === 0) {
+ continue;
+ }
+
+ // Number of entities to be processed for each step.
+ $info = entity_get_info($entity_type);
+ $offset = $context['sandbox']['progress_entity_type'][$entity_type];
+ $query = entity_query($entity_type);
+ $result = $query
+ ->exists($field_name)
+ ->sort($info['entity_keys']['id'])
+ ->range($offset, 10)
+ ->execute();
+
+ foreach (entity_load_multiple($entity_type, $result) as $id => $entity) {
+ $context['sandbox']['max_entity_type'][$entity_type] -= count($result);
+ $context['sandbox']['progress_entity_type'][$entity_type]++;
+ $context['sandbox']['progress']++;
+ $langcode = $entity->language()->langcode;
+
+ // Skip process for language neutral entities.
+ if ($langcode == LANGUAGE_NOT_SPECIFIED) {
+ continue;
+ }
+
+ // We need a two-steps approach while updating field translations: given
+ // that field-specific update functions might rely on the stored values to
+ // perform their processing, see for instance file_field_update(), first
+ // we need to store the new translations and only after we can remove the
+ // old ones. Otherwise we might have data loss, since the removal of the
+ // old translations might occur before the new ones are stored.
+ if ($translatable && isset($entity->{$field_name}[LANGUAGE_NOT_SPECIFIED])) {
+ // If the field is being switched to translatable and has data for
+ // LANGUAGE_NOT_SPECIFIED then we need to move the data to the right
+ // language.
+ $entity->{$field_name}[$langcode] = $entity->{$field_name}[LANGUAGE_NOT_SPECIFIED];
+ // Store the original value.
+ _translation_entity_update_field($entity_type, $entity, $field_name);
+ $entity->{$field_name}[LANGUAGE_NOT_SPECIFIED] = array();
+ // Remove the language neutral value.
+ _translation_entity_update_field($entity_type, $entity, $field_name);
+ }
+ elseif (!$translatable && isset($entity->{$field_name}[$langcode])) {
+ // The field has been marked untranslatable and has data in the entity
+ // language: we need to move it to LANGUAGE_NOT_SPECIFIED and drop the
+ // other translations.
+ $entity->{$field_name}[LANGUAGE_NOT_SPECIFIED] = $entity->{$field_name}[$langcode];
+ // Store the original value.
+ _translation_entity_update_field($entity_type, $entity, $field_name);
+ // Remove translations.
+ foreach ($entity->{$field_name} as $langcode => $items) {
+ if ($langcode != LANGUAGE_NOT_SPECIFIED) {
+ $entity->{$field_name}[$langcode] = array();
+ }
+ }
+ _translation_entity_update_field($entity_type, $entity, $field_name);
+ }
+ else {
+ // No need to save unchanged entities.
+ continue;
+ }
+ }
+ }
+
+ $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
+}
+
+/**
+ * Stores the given field translations.
+ */
+function _translation_entity_update_field($entity_type, $entity, $field_name) {
+ $empty = 0;
+ $field = field_info_field($field_name);
+
+ // Ensure that we are trying to store only valid data.
+ foreach ($entity->{$field_name} as $langcode => $items) {
+ $entity->{$field_name}[$langcode] = _field_filter_items($field, $entity->{$field_name}[$langcode]);
+ $empty += empty($entity->{$field_name}[$langcode]);
+ }
+
+ // Save the field value only if there is at least one item available,
+ // otherwise any stored empty field value would be deleted. If this happens
+ // the range queries would be messed up.
+ if ($empty < count($entity->{$field_name})) {
+ field_attach_presave($entity_type, $entity);
+ field_attach_update($entity_type, $entity);
+ }
+}
+
+/**
+ * Checks the exit status of the batch operation.
+ */
+function translation_entity_translatable_batch_done($success, $results, $operations) {
+ if ($success) {
+ drupal_set_message(t("Successfully changed field translation setting."));
+ }
+ else {
+ // @todo: Do something about this case.
+ drupal_set_message(t("Something went wrong while processing data. Some nodes may appear to have lost fields."), 'error');
+ }
+}
+
diff --git a/core/modules/translation_entity/translation_entity.info b/core/modules/translation_entity/translation_entity.info
new file mode 100644
index 0000000..4a28def
--- /dev/null
+++ b/core/modules/translation_entity/translation_entity.info
@@ -0,0 +1,6 @@
+name = Entity Translation
+description = Allows entities to be translated into different languages.
+dependencies[] = language
+package = Core
+version = VERSION
+core = 8.x
diff --git a/core/modules/translation_entity/translation_entity.install b/core/modules/translation_entity/translation_entity.install
new file mode 100644
index 0000000..b571da3
--- /dev/null
+++ b/core/modules/translation_entity/translation_entity.install
@@ -0,0 +1,71 @@
+ 'Table to track entity translations',
+ 'fields' => array(
+ 'entity_type' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The entity type this translation relates to',
+ ),
+ 'entity_id' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'The entity id this translation relates to',
+ ),
+ 'langcode' => array(
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The target language for this translation.',
+ ),
+ 'source' => array(
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The source language from which this translation was created.',
+ ),
+ 'translate' => array(
+ 'description' => 'A boolean indicating whether this translation needs to be updated.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('entity_type', 'entity_id', 'langcode'),
+ );
+ return $schema;
+}
+
+/**
+ * Implements hook_install().
+ */
+function translation_entity_install() {
+ language_negotiation_include();
+ language_negotiation_set(LANGUAGE_TYPE_CONTENT, array(LANGUAGE_NEGOTIATION_URL => 0));
+}
+
+/**
+ * Implements hook_enable().
+ */
+function translation_entity_enable() {
+ $t_args = array(
+ '!language_url' => url('admin/config/regional/language'),
+ );
+ $message = t('You just added content translation capabilities to your site. To exploit them be sure to enable at least two languages and enable translation for content types, taxonomy vocabularies, accounts and any other element whose content you wish to translate.', $t_args);
+ drupal_set_message($message, 'warning');
+}
diff --git a/core/modules/translation_entity/translation_entity.module b/core/modules/translation_entity/translation_entity.module
new file mode 100644
index 0000000..a530c65
--- /dev/null
+++ b/core/modules/translation_entity/translation_entity.module
@@ -0,0 +1,692 @@
+' . t('About') . '';
+ $output .= '
' . t('The Entity Translation module allows you to create and manage translations for your Drupal site content. You can specify which elements need to be translated at the content-type level for content items and comments, at the vocabulary level for taxonomy terms, and at the site level for user accounts. Other modules may provide additional elements that can be translated. For more information, see the online handbook entry for Entity Translation.', array('!url' => 'http://drupal.org/documentation/modules/entity_translation')) . '
'; + $output .= '' . t('Before you can translate content, there must be at least two non-system languages added on the languages administration page.', array('!url' => url('admin/config/regional/language'))) . '
'; + $output .= '' . t('After adding languages, enable translation for any content you wish to translate:') . '
'; + $output .= '' . t('Finally, under the Manage fields tab, edit each field you wish to be translatable, and enable or disable translation under Global settings.') . '