Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.802
diff -u -r1.802 comment.module
--- modules/comment/comment.module 7 Nov 2009 13:35:20 -0000 1.802
+++ modules/comment/comment.module 7 Nov 2009 16:58:41 -0000
@@ -696,13 +696,17 @@
$query
->condition('c.nid', $node->nid)
->addTag('node_access')
+ ->addTag('comment_filter')
+ ->addMetaData('node', $node)
->limit($comments_per_page);
$count_query = db_select('comment', 'c');
$count_query->addExpression('COUNT(*)');
$count_query
->condition('c.nid', $node->nid)
- ->addTag('node_access');
+ ->addTag('node_access')
+ ->addTag('comment_filter')
+ ->addMetaData('node', $node);
if (!user_access('administer comments')) {
$query->condition('c.status', COMMENT_PUBLISHED);
Index: modules/entity_translation/entity_translation.info
===================================================================
RCS file: modules/entity_translation/entity_translation.info
diff -N modules/entity_translation/entity_translation.info
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.info 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,13 @@
+; $Id$
+name = Entity translation
+description = Allow fieldable entities to be translated into different languages.
+dependencies[] = locale
+package = Core
+version = VERSION
+core = 7.x
+files[] = entity_translation.install
+files[] = entity_translation.module
+files[] = entity_translation.admin.inc
+files[] = entity_translation.handler.inc
+files[] = entity_translation.handler.node.inc
+files[] = entity_translation.node.inc
Index: modules/entity_translation/entity_translation.handler.inc
===================================================================
RCS file: modules/entity_translation/entity_translation.handler.inc
diff -N modules/entity_translation/entity_translation.handler.inc
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.handler.inc 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,555 @@
+entityType = $entity_type;
+ $this->entityInfo = $entity_info;
+ $this->entity = $entity;
+ $this->entityId = $entity_id;
+
+ $this->translating = FALSE;
+ $this->outdated = FALSE;
+
+ $info = $entity_info['translation']['entity_translation'];
+ $this->basePath = $this->getPathInstance($info['base path']);
+ $this->editPath = isset($info['edit path']) ? $this->getPathInstance($info['edit path']) : FALSE;
+ $this->viewPath = isset($info['view path']) ? $this->getPathInstance($info['view path']) : FALSE;
+ }
+
+ public function load() {
+ if (isset($this->entityId)) {
+ $this->loadMultiple($this->entityType, array($this->entityId => $this->entity));
+ }
+ else {
+ $this->entity->{$this->getTranslationsKey()} = $this->emptyTranslations();
+ }
+ }
+
+ public function save() {
+ $this->writeTranslations();
+ }
+
+ public function isTranslating() {
+ return $this->translating;
+ }
+
+ public function setTranslating($translating) {
+ $this->translating = $translating;
+ }
+
+ public function isRevision() {
+ return FALSE;
+ }
+
+ public function prepareRevision($langcode) {
+ // Load into $stored_entity the field values as currently stored.
+ $stored_entity = clone($this->entity);
+ field_attach_load($this->entityType, array($this->getEntityId() => $stored_entity));
+ list(, , $bundle) = entity_extract_ids($this->entityType, $this->entity);
+
+ foreach (field_info_instances($this->entityType, $bundle) as $instance) {
+ $field_name = $instance['field_name'];
+ $field = field_info_field($field_name);
+
+ if ($field['translatable']) {
+ // For each translatable field copy into the wrapped entity the field
+ // values as currently stored, except the ones having language equal to
+ // the one currently being edited. This way every time a new revision is
+ // created for a certain language all of its translations get a new
+ // revision.
+ foreach ($stored_entity->{$field_name} as $stored_langcode => $items) {
+ if ($stored_langcode != $langcode) {
+ $this->entity->{$field_name}[$stored_langcode] = $items;
+ }
+ }
+ }
+ }
+ }
+
+ public function getTranslations() {
+ $translations_key = $this->getTranslationsKey();
+
+ if (!isset($this->entity->{$translations_key})) {
+ $this->load();
+ }
+
+ return $this->entity->{$translations_key};
+ }
+
+ public function setTranslation($translation, $values = NULL) {
+ if (isset($translation['source']) && $translation['language'] == $translation['source']) {
+ throw new Exception('Invalid translation language');
+ }
+
+ $translations = $this->getTranslations();
+ $langcode = $translation['language'];
+
+ $this->setTranslating(TRUE);
+
+ if (isset($translations->data[$langcode])) {
+ $translation = array_merge($translations->data[$langcode], $translation);
+ $translation['changed'] = REQUEST_TIME;
+ }
+
+ $translations->data[$langcode] = $translation;
+
+ if (is_array($values)) {
+ // Update field translations.
+ list(, , $bundle) = entity_extract_ids($this->entityType, $this->entity);
+ foreach (field_info_instances($this->entityType, $bundle) as $instance) {
+ $field_name = $instance['field_name'];
+ $field = field_info_field($field_name);
+ if ($field['translatable'] && isset($values[$field_name])) {
+ $this->entity->{$field_name}[$langcode] = $values[$field_name][$langcode];
+ }
+ }
+ }
+ }
+
+ public function removeTranslation($langcode) {
+ $translations_key = $this->getTranslationsKey();
+
+ if (!empty($langcode)) {
+ unset($this->entity->{$translations_key}->data[$langcode]);
+ }
+ else {
+ $this->entity->{$translations_key}->data = array();
+ }
+
+ list(, , $bundle) = entity_extract_ids($this->entityType, $this->entity);
+
+ // Remove field translations.
+ foreach (field_info_instances($this->entityType, $bundle) as $instance) {
+ $field_name = $instance['field_name'];
+ $field = field_info_field($field_name);
+
+ if ($field['translatable']) {
+ if (!empty($langcode)) {
+ $this->entity->{$field_name}[$langcode] = array();
+ }
+ else {
+ $this->entity->{$field_name} = array();
+ }
+ }
+ }
+ }
+
+ public function initTranslations() {
+ $langcode = $this->getLanguage();
+
+ if (!empty($langcode)) {
+ $translation = array('language' => $langcode, 'status' => $this->getStatus());
+ $this->setTranslation($translation);
+ $this->setOriginalLanguage($langcode);
+ }
+ }
+
+ public function clearTranslations() {
+ $this->removeTranslation(FALSE);
+ }
+
+ public function updateTranslations() {
+ $langcode = $this->getLanguage();
+
+ // Only create a translation on edit if the translation set is empty:
+ // the entity might have been created with language set to "language
+ // neutral".
+ if (empty($this->getTranslations()->data)) {
+ $this->initTranslations();
+ }
+ elseif (!empty($langcode) && !$this->isTranslating()) {
+ $this->setOriginalLanguage($langcode);
+ }
+
+ if ($this->isRevision()) {
+ $this->prepareRevision($langcode);
+ }
+ }
+
+ public function initOriginalTranslation() {
+ $fixed = FALSE;
+ $translations = $this->getTranslations();
+ list(, , $bundle) = entity_extract_ids($this->entityType, $this->entity);
+
+ foreach (field_info_instances($this->entityType, $bundle) as $instance) {
+ $field_name = $instance['field_name'];
+ $field = field_info_field($field_name);
+ $langcode = count($this->entity->{$field_name}) == 1 ? key($this->entity->{$field_name}) : $translations->original;
+
+ if ($langcode == FIELD_LANGUAGE_NONE && $field['translatable']) {
+ $this->entity->{$field_name}[$translations->original] = $this->entity->{$field_name}[$langcode];
+ $this->entity->{$field_name}[$langcode] = array();
+ $fixed = TRUE;
+ }
+ }
+
+ return $fixed;
+ }
+
+ public function setEntity($entity) {
+ $this->entity = $entity;
+ }
+
+
+ public function getLanguage() {
+ return language_default()->language;
+ }
+
+ public function setLanguage($langcode) {
+ $this->language = $langcode;
+ }
+
+ public function setOriginalLanguage($langcode) {
+ $translations = $this->getTranslations();
+
+ if (isset($translations->original) && $translations->original != $langcode) {
+ $translations->data[$langcode] = $translations->data[$translations->original];
+ $translations->data[$langcode]['language'] = $langcode;
+ unset($translations->data[$translations->original]);
+ }
+
+ $translations->original = $langcode;
+ }
+
+ public function setOutdated($outdated) {
+ $this->outdated = $outdated;
+ }
+
+ public function getBasePath() {
+ return $this->basePath;
+ }
+
+ public function getViewPath() {
+ return $this->viewPath;
+ }
+
+ public function getEditPath() {
+ return $this->editPath;
+ }
+
+ public function getHumanReadableId() {
+ if (isset($this->entityInfo['object keys']['human readable id'])) {
+ $key = $this->entityInfo['object keys']['human readable id'];
+ return $this->entity->{$key};
+ }
+ else {
+ return "{$this->entityType}:{$this->getEntityId()}" ;
+ }
+ }
+
+ public function getAccess($op) {
+ return TRUE;
+ }
+
+ public function isAliasEnabled() {
+ return !empty($this->entityInfo['translation']['entity_translation']['alias']);
+ }
+
+ /**
+ * Return the translation object key for the wrapped entity type.
+ */
+ protected function getTranslationsKey() {
+ return $this->entityInfo['object keys']['translations'];
+ }
+
+ /**
+ * Return the entity accessibility.
+ */
+ protected function getStatus() {
+ return TRUE;
+ }
+
+ /**
+ * Return the entity identifier.
+ */
+ protected function getEntityId() {
+ return $this->entityId;
+ }
+
+ /**
+ * Return the entity type identifier.
+ */
+ protected function getEtid() {
+ // @todo: Consider avoiding the use of a field SQL storage function to
+ // identify the entity: we might have different storage engines.
+ return _field_sql_storage_etid($this->entityType);
+ }
+
+ /**
+ * Return an instance of the given path.
+ *
+ * @param $path
+ * An internal path containing the entity id wildcard.
+ *
+ * @return
+ * The instantiated path.
+ */
+ protected function getPathInstance($path) {
+ $wildcard = $this->entityInfo['translation']['entity_translation']['path wildcard'];
+ return str_replace($wildcard, $this->getEntityId(), $path);
+ }
+
+ /**
+ * Write the translation data to the storage.
+ */
+ protected function writeTranslations() {
+ $etid = $this->getEtid();
+
+ // Delete and insert, rather than update, in case a value was added.
+ db_delete('entity_translation')
+ ->condition('etid', $etid)
+ ->condition('entity_id', $this->entityId)
+ ->execute();
+
+ $translations = $this->getTranslations();
+
+ if (count($translations->data)) {
+ global $user;
+
+ $columns = array('etid', 'entity_id', 'language', 'source', 'uid', 'status', 'translate', 'created', 'changed');
+ $query = db_insert('entity_translation')->fields($columns);
+
+ foreach ($translations->data as $langcode => $translation) {
+
+ $translation += array(
+ 'etid' => $etid,
+ 'entity_id' => $this->entityId,
+ 'source' => '',
+ 'uid' => $user->uid,
+ 'translate' => 0,
+ 'status' => 0,
+ 'created' => REQUEST_TIME,
+ 'changed' => REQUEST_TIME,
+ );
+
+ if ($this->outdated && $langcode != $translations->original) {
+ $translation['translate'] = 1;
+ }
+
+ $query->values($translation);
+ }
+
+ $query->execute();
+ }
+ }
+
+ /**
+ * Read the translation data from the storage.
+ */
+ public static function loadMultiple($entity_type, $entities) {
+ $etid = _field_sql_storage_etid($entity_type);
+ $entity_info = entity_get_info($entity_type);
+ $translations_key = $entity_info['object keys']['translations'];
+
+ foreach ($entities as $id => $entity) {
+ $entities[$id]->{$translations_key} = EntityTranslationDefaultHandler::emptyTranslations();
+ }
+
+ $results = db_select('entity_translation', 'et')
+ ->fields('et')
+ ->condition('etid', $etid)
+ ->condition('entity_id', array_keys($entities), 'IN')
+ ->orderBy('entity_id')
+ ->orderBy('created')
+ ->execute();
+
+ foreach ($results as $row) {
+ $id = $row->entity_id;
+ $entities[$id]->{$translations_key}->data[$row->language] = (array) $row;
+
+ // Only the original translation has an empty source.
+ if (empty($row->source)) {
+ $entities[$id]->{$translations_key}->original = $row->language;
+ }
+ }
+ }
+
+ /**
+ * Return an empty translations data structure.
+ */
+ protected static function emptyTranslations() {
+ return (object) array('original' => NULL, 'data' => array());
+ }
+}
Index: modules/entity_translation/entity-translation-node-form.js
===================================================================
RCS file: modules/entity_translation/entity-translation-node-form.js
diff -N modules/entity_translation/entity-translation-node-form.js
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity-translation-node-form.js 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,15 @@
+// $Id$
+
+(function ($) {
+
+Drupal.behaviors.translationNodeFieldsetSummaries = {
+ attach: function (context) {
+ $('fieldset#edit-entity-translation', context).setSummary(function (context) {
+ return $('#edit-entity-translation-retranslate', context).is(':checked') ?
+ Drupal.t('Flag translations as outdated') :
+ Drupal.t('Don\'t flag translations as outdated');
+ });
+ }
+};
+
+})(jQuery);
Index: modules/entity_translation/modules/entity_translation_upgrade/entity_translation_upgrade.install
===================================================================
RCS file: modules/entity_translation/modules/entity_translation_upgrade/entity_translation_upgrade.install
diff -N modules/entity_translation/modules/entity_translation_upgrade/entity_translation_upgrade.install
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/modules/entity_translation_upgrade/entity_translation_upgrade.install 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,249 @@
+ 'The history table for node translations.',
+ 'fields' => array(
+ 'nid' => array(
+ 'description' => 'The node translation nid.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'tnid' => array(
+ 'description' => 'The translation set id for the node translation.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'language' => array(
+ 'description' => 'The node translation language.',
+ 'type' => 'varchar',
+ 'length' => 12,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ ),
+ 'indexes' => array('tnid' => array('tnid')),
+ 'primary key' => array('nid'),
+ );
+
+ return $schema;
+}
+
+/**
+ * Start the batch process to perform the upgrade.
+ */
+function entity_translation_upgrade_start() {
+ $batch = array(
+ 'operations' => array(
+ array('entity_translation_upgrade_do', array()),
+ ),
+ 'finished' => 'entity_translation_upgrade_end',
+ 'title' => t('Entity Translation Upgrade'),
+ 'init_message' => t('Entity Translation Upgrade is starting.'),
+ 'error_message' => t('Entity Translation Upgrade has encountered an error.'),
+ 'file' => drupal_get_path('module', 'entity_translation_upgrade') . '/entity_translation_upgrade.install',
+ );
+ batch_set($batch);
+ batch_process('admin/config/modules');
+}
+
+/**
+ * Finshed batch callback.
+ */
+function entity_translation_upgrade_end($success, $results, $oprations, $elapsed) {
+ $message = format_plural($results, '1 node translation successfully upgraded.', '@count node translations successfully upgraded.');
+ watchdog('entity translation upgrade', '@count node translations successfully upgraded.', array('@count' => $results), WATCHDOG_INFO);
+ drupal_set_message($message);
+}
+
+/**
+ * Batch process to convert node translations to field-based translations.
+ */
+function entity_translation_upgrade_do(&$context) {
+ $query = db_select('node', 'n')
+ ->fields('n', array('nid', 'tnid'))
+ ->condition('tnid', 0, '!=')
+ ->condition('tnid != nid', array(), '')
+ ->condition('nid NOT IN (SELECT nid FROM {entity_translation_upgrade_history})', array(), '')
+ ->orderBy('nid');
+
+ // Initialize the batch process.
+ if (empty($context['sandbox'])) {
+ $total = $query
+ ->countQuery()
+ ->execute()
+ ->fetchField();
+
+ $context['sandbox']['count'] = 0;
+ $context['sandbox']['total'] = $total;
+ $context['finished'] = $total == 0;
+ }
+ else {
+ $result = $query
+ ->range($context['sandbox']['count'], ENTITY_TRANSLATION_UPGRADE_BATCH_SIZE)
+ ->execute()
+ ->fetchAllKeyed();
+
+ // Here we load original nodes and translations all together, but we count
+ // only node translations with respect to the batch size.
+ $nids = array_keys($result);
+ $nodes = node_load_multiple($nids + array_unique($result));
+
+ $updated_nodes = array();
+ $node_translations = array();
+ $node_translation_sets = array();
+ $instances = array();
+ $field_info = array();
+
+ foreach ($nodes as $node) {
+ if ($node->tnid != $node->nid) {
+ $original = $nodes[$node->tnid];
+ $handler = entity_translation_get_handler('node', $original);
+
+ if (!isset($instances[$node->type])) {
+ $instances[$node->type] = field_info_instances('node', $node->type);
+ }
+
+ reset($instances[$node->type]);
+
+ foreach ($instances[$node->type] as $instance) {
+ $field_name = $instance['field_name'];
+ $field = isset($field_info[$field_name]) ? $field_info[$field_name] : $field_info[$field_name] = field_info_field($field_name);
+
+ // Copy field data.
+ if ($field['translatable']) {
+ $langcode = isset($node->{$field_name}[$node->language]) ? $node->language : FIELD_LANGUAGE_NONE;
+ if (isset($node->{$field_name}[$langcode])) {
+ $original->{$field_name}[$node->language] = $node->{$field_name}[$langcode];
+ }
+ }
+ }
+
+ // Add the new translation.
+ $handler->setTranslation(array(
+ 'translate' => $node->translate,
+ 'status' => $node->status,
+ 'language' => $node->language,
+ 'source' => $original->language,
+ 'uid' => $node->uid,
+ 'created' => $node->created,
+ 'changed' => $node->changed,
+ )
+ );
+
+ // Build a list of updated nodes. They will be saved after all the node
+ // translation conversions.
+ $updated_nodes[$original->nid] = $original;
+
+ // Build a list of obsolete node translations to be unpublished.
+ $node_translations[$node->nid] = $node;
+
+ // Build a list of obsolete translations sets to be passed to module hook
+ // implementations.
+ $node_translation_sets[$original->nid][$node->nid] = $node;
+
+ $context['sandbox']['count']++;
+ }
+ }
+
+ // Ensure that the multilingual support configuration is set to the right
+ // value for the current node type.
+ foreach ($instances as $type_name => $data) {
+ variable_set("language_content_type_$type_name", 1);
+ }
+
+ // Save field data and translations for updated nodes.
+ foreach ($updated_nodes as $nid => $node) {
+ field_attach_update('node', $node);
+ entity_translation_get_handler('node', $node)->save();
+
+ foreach ($node_translation_sets[$nid] as $translation) {
+ // Allow modules to upgrade their node additions, if possible.
+ module_invoke_all('entity_translation_upgrade_translation', $node, $translation);
+ }
+ }
+
+ $nids = array_keys($node_translations);
+
+ // Unpublish the obsolete node translations.
+ db_update('node')
+ ->fields(array('status' => 0))
+ ->condition('nid', $nids)
+ ->execute();
+
+ db_update('node_revision')
+ ->fields(array('status' => 0))
+ ->condition('nid', $nids)
+ ->execute();
+
+ if (!empty($node_translations)) {
+ // Populate history table.
+ $columns = array('nid', 'tnid', 'language');
+ $query = db_insert('entity_translation_upgrade_history')->fields($columns);
+
+ foreach ($node_translations as $node) {
+ $query->values((array) $node);
+ }
+
+ $query->execute();
+ }
+
+ $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
+
+ if ($context['finished']) {
+ $context['results'] = $context['sandbox']['total'];
+ }
+ }
+}
+
+/**
+ * Implement hook_entity_translation_upgrade_translation().
+ */
+function entity_translation_upgrade_entity_translation_upgrade_translation($node, $translation) {
+ // Attach comments to the original node.
+ db_update('comment')
+ ->fields(array('nid' => $node->nid, 'language' => $translation->language))
+ ->condition('nid', $translation->nid)
+ ->execute();
+
+ // Update node-comment statistics.
+ $ncs = db_select('node_comment_statistics', 'ncs')
+ ->fields('ncs')
+ ->condition('nid', array($node->nid, $translation->nid))
+ ->execute()
+ ->fetchAllAssoc('nid');
+
+ $last = $ncs[$node->nid]->last_comment_timestamp > $ncs[$translation->nid]->last_comment_timestamp;
+ $ncs_updated = $last ? $ncs[$node->nid] : $ncs[$translation->nid];
+ $ncs_updated->nid = $node->nid;
+ $ncs_updated->comment_count = $ncs[$node->nid]->comment_count + $ncs[$translation->nid]->comment_count;
+
+ db_update('node_comment_statistics')
+ ->fields((array) $ncs_updated)
+ ->condition('nid', $node->nid)
+ ->execute();
+}
Index: modules/entity_translation/entity_translation.module
===================================================================
RCS file: modules/entity_translation/entity_translation.module
diff -N modules/entity_translation/entity_translation.module
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.module 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,506 @@
+ array(
+ 'object keys' => array(
+ 'human readable id' => 'subject',
+ ),
+ ),
+ 'node' => array(
+ 'translation' => array(
+ 'entity_translation' => array(
+ 'class' => 'EntityTranslationNodeHandler',
+ 'base path' => 'node/%node',
+ 'edit path' => 'node/%node/edit',
+ 'path wildcard' => '%node',
+ 'alias' => TRUE,
+ 'access callback' => 'entity_translation_node_tab_access',
+ 'access arguments' => array(1),
+ 'edit form' => array(
+ 'form id' => 'node-form',
+ 'entity key' => '#node',
+ ),
+ ),
+ ),
+ 'object keys' => array(
+ 'human readable id' => 'title',
+ ),
+ ),
+ 'taxonomy_term' => array(
+ 'translation' => array(
+ 'entity_translation' => array(
+ 'base path' => 'taxonomy/term/%taxonomy_term',
+ 'edit path' => 'taxonomy/term/%taxonomy_term/edit',
+ 'path wildcard' => '%taxonomy_term',
+ 'alias' => TRUE,
+ 'edit form' => array(
+ 'form id' => 'taxonomy-form-term',
+ 'entity key' => '#term',
+ ),
+ ),
+ ),
+ 'object keys' => array(
+ 'human readable id' => 'name',
+ ),
+ ),
+ 'user' => array(
+ 'translation' => array(
+ 'entity_translation' => array(
+ 'base path' => 'user/%user',
+ 'edit path' => 'user/%user/edit',
+ 'path wildcard' => '%user',
+ 'edit form' => array(
+ 'form id' => 'user-profile-form',
+ 'entity key' => '#user',
+ ),
+ ),
+ ),
+ 'object keys' => array(
+ 'human readable id' => 'name',
+ ),
+ ),
+ );
+
+ return isset($types) ? array_intersect_key($info, $types) : $info;
+}
+
+/**
+ * Implement hook_entity_info_alter().
+ */
+function entity_translation_entity_info_alter(&$entity_info) {
+ // Collect entity-specific translation information.
+ $types = array_flip(array_keys($entity_info));
+ $translation_info = module_invoke_all('translation_info', $types);
+ $entity_info = array_merge_recursive($entity_info, $translation_info);
+ $edit_form_info = array();
+
+ // Provide defaults for translation info.
+ foreach (entity_get_info() as $entity_type => $info) {
+ if (entity_translation_entity_type_enabled($entity_type, TRUE)) {
+ if (!isset($entity_info[$entity_type]['translation']['entity_translation'])) {
+ $entity_info[$entity_type]['translation']['entity_translation'] = array();
+ }
+
+ if (!isset($entity_info[$entity_type]['translation']['entity_translation']['base path'])) {
+ $entity_info[$entity_type]['translation']['entity_translation'] += array(
+ 'base path' => "entity/{$entity_type}/%entity_translation_menu_entity",
+ 'load arguments' => array($entity_type, 2),
+ );
+ }
+ else {
+ $entity_info[$entity_type]['translation']['entity_translation']['view path'] = $entity_info[$entity_type]['translation']['entity_translation']['base path'];
+ }
+
+ $entity_info[$entity_type]['translation']['entity_translation'] += array(
+ 'class' => 'EntityTranslationDefaultHandler',
+ 'path wildcard' => '%entity_translation_entity',
+ 'access callback' => 'entity_translation_tab_access',
+ 'access arguments' => array($entity_type),
+ 'theme callback' => 'variable_get',
+ 'theme arguments' => array('admin_theme'),
+ );
+
+ $entity_info[$entity_type]['object keys'] += array(
+ 'translations' => 'translations',
+ );
+
+ // Prepare edit form info.
+ if (isset($entity_info[$entity_type]['translation']['entity_translation']['edit form'])) {
+ $info = $entity_info[$entity_type]['translation']['entity_translation']['edit form'];
+ $form_id = $info['form id'];
+ $edit_form_info[$form_id] = $info;
+ $edit_form_info[$form_id]['entity type'] = $entity_type;
+ }
+ }
+ }
+
+ variable_set('entity_translation_edit_form_info', $edit_form_info);
+}
+
+/**
+ * Helper function to determine if the given entity type is translatable.
+ */
+function entity_translation_entity_type_enabled($entity_type, $skip_handler = FALSE) {
+ $enabled_types = variable_get('entity_translation_entity_types', array());
+ return
+ isset($enabled_types[$entity_type]) &&
+ $enabled_types[$entity_type] &&
+ ($skip_handler || field_multilingual_check_translation_handlers($entity_type, 'entity_translation'));
+}
+
+/**
+ * Implement hook_menu().
+ */
+function entity_translation_menu() {
+ $items = array();
+
+ // Refresh fieldable types info.
+ field_info_cache_clear();
+
+ // Create tabs for all possible bundles.
+ foreach (entity_get_info() as $entity_type => $info) {
+ if (isset($info['translation']['entity_translation']['base path'])) {
+ // Extract informations from the bundle description.
+ $path = $info['translation']['entity_translation']['base path'];
+ $keys = array('theme callback', 'theme arguments', 'access callback', 'access arguments', 'load arguments');
+ $item = array_intersect_key($info['translation']['entity_translation'], drupal_map_assoc($keys));
+
+ $entity_position = count(explode('/', $path)) - 1;
+ $source_position = $entity_position + 3;
+ $language_position = $entity_position + 4;
+
+ $items["$path/entity-translation"] = array(
+ 'title' => 'Translate',
+ 'page callback' => 'entity_translation_overview',
+ 'page arguments' => array($entity_type, $entity_position),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 2,
+ 'file' => 'entity_translation.admin.inc',
+ ) + $item;
+
+ $items["$path/entity-translation/overview"] = array(
+ 'title' => 'Overview',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => 0,
+ );
+
+ $items["$path/entity-translation/translate/%entity_translation_language/%entity_translation_language"] = array(
+ 'title' => 'Translate',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('entity_translation_translation_form', $entity_type, $entity_position, $source_position, $language_position),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 1,
+ 'file' => 'entity_translation.admin.inc',
+ ) + $item;
+
+ $items["$path/entity-translation/delete/%entity_translation_language/%entity_translation_language"] = array(
+ 'title' => 'Delete',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('entity_translation_translation_delete_confirm', $entity_type, $entity_position, $source_position, $language_position),
+ 'file' => 'entity_translation.admin.inc',
+ ) + $item;
+ }
+ }
+
+ $items['admin/config/regional/entity-translation'] = array(
+ 'title' => 'Entity translation settings',
+ 'description' => 'Select which entities can be translated.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('entity_translation_admin_form'),
+ 'access arguments' => array('administer entity translation'),
+ 'file' => 'entity_translation.admin.inc',
+ );
+
+ return $items;
+}
+
+/**
+ * Access callback.
+ */
+function entity_translation_tab_access($entity_type) {
+ return drupal_multilingual() && (user_access('translate all entities') || user_access('translate ' . $entity_type . ' entities'));
+}
+
+/**
+ * Menu loader callback.
+ */
+function entity_translation_language_load($langcode) {
+ $enabled_languages = field_multilingual_content_languages();
+ return in_array($langcode, $enabled_languages) ? $langcode : FALSE;
+}
+
+/**
+ * Menu loader callback.
+ */
+function entity_translation_menu_entity_load($entity_id, $entity_type) {
+ $entities = entity_load($entity_type, array($entity_id));
+ return $entities[$entity_id];
+}
+
+/**
+ * Implement hook_permission().
+ */
+function entity_translation_permission() {
+ $permission = array(
+ 'translate all entities' => array(
+ 'title' => t('Translate all entities'),
+ 'description' => t('Translate field content for any fieldable entity.'),
+ ),
+ 'administer entity translation' => array(
+ 'title' => t('Administer entity translation'),
+ 'description' => t('Select which entities can be translated.'),
+ ),
+ );
+
+ foreach (entity_get_info() as $entity_type => $info) {
+ if ($info['fieldable']) {
+ $label = t($info['label']);
+ $permission['translate ' . $entity_type . ' entities'] = array(
+ 'title' => t('Translate entities of type @type', array('@type' => $label)),
+ 'description' => t('Translate field content for entities of type @type', array('@type' => $label)),
+ );
+ }
+ }
+
+ return $permission;
+}
+
+/**
+ * Implement hook_theme().
+ */
+function entity_translation_theme() {
+ return array(
+ 'entity_translation_unavailable' => array(
+ 'variables' => array('element' => NULL),
+ ),
+ );
+}
+
+/**
+ * Implement hook_entity_load().
+ */
+function entity_translation_entity_load($entities, $entity_type) {
+ if (entity_translation_entity_type_enabled($entity_type)) {
+ EntityTranslationDefaultHandler::loadMultiple($entity_type, $entities);
+ }
+}
+
+/**
+ * Implement hook_field_attach_view_alter().
+ */
+function entity_translation_field_attach_view_alter(&$output, $context) {
+ // In locale_field_fallback_view() we might call field_attach_view(). The
+ // static variable avoids unnecessary recursion.
+ static $recursion;
+
+ if (!$recursion && entity_translation_entity_type_enabled($context['obj_type'])) {
+ global $language;
+
+ $recursion = TRUE;
+ $entity = $context['object'];
+ $entity_type = $context['obj_type'];
+ $handler = entity_translation_get_handler($entity_type, $context['object']);
+ $translations = $handler->getTranslations();
+ $langcode = $language->language;
+
+ // Provide context for rendering.
+ $output['#entity'] = $entity;
+ $output['#entity_type'] = $entity_type;
+
+ if (!isset($translations->data[$langcode]) || !entity_translation_access($entity_type, $translations->data[$langcode])) {
+ $fallback = variable_get('locale_field_fallback_view', TRUE);
+
+ // Apply fallback only on unpublished translations as missing translations
+ // are already handled in locale_field_attach_view_alter().
+ if ($fallback && isset($translations->data[$langcode])) {
+ $entity = clone($entity);
+
+ list(, , $bundle) = entity_extract_ids($entity_type, $entity);
+ foreach (field_info_instances($entity_type, $bundle) as $instance) {
+ $field_name = $instance['field_name'];
+ $field = field_info_field($field_name);
+ if ($field['translatable']) {
+ unset($entity->{$field_name}[$langcode]);
+ unset($output[$field_name]['items']);
+ }
+ }
+
+ $context['object'] = $entity;
+ locale_field_fallback_view($output, $context);
+ }
+ // If fallback is disabled we need to notify the user the translation is
+ // unavailable (missing or unpublished).
+ else if (!$fallback) {
+ // Perform theming here because the default theming needs to set system
+ // messages. It would be too late in the #post_render callback.
+ $output['#entity_translation_unavailable'] = theme('entity_translation_unavailable', array('element' => $output));
+ $output['#post_render'][] = 'entity_translation_unavailable';
+ }
+ }
+
+ $recursion = FALSE;
+ }
+}
+
+/**
+ * Override the entity output with the unavailable translation one.
+ */
+function entity_translation_unavailable($children, $element) {
+ return $element['#entity_translation_unavailable'];
+}
+
+/**
+ * Theme an unvailable translation.
+ */
+function theme_entity_translation_unavailable($variables) {
+ global $language;
+
+ $element = $variables['element'];
+ $handler = entity_translation_get_handler($element['#entity_type'], $element['#entity']);
+ $args = array('%language' => t($language->name), '%id' => $handler->getHumanReadableId());
+ drupal_set_message(t('%language translation unavailable for %id.', $args), 'warning');
+
+ // Hide the unavailable translation.
+ return '';
+}
+
+/**
+ * Implement hook_field_attach_pre_insert().
+ */
+function entity_translation_field_attach_pre_insert($obj_type, $object) {
+ if (entity_translation_entity_type_enabled($obj_type)) {
+ $handler = entity_translation_get_handler($obj_type, $object);
+ $handler->initTranslations();
+ $handler->save();
+ }
+}
+
+/**
+ * Implement hook_field_attach_pre_update().
+ */
+function entity_translation_field_attach_pre_update($obj_type, $object) {
+ if (entity_translation_entity_type_enabled($obj_type)) {
+ $handler = entity_translation_get_handler($obj_type, $object, TRUE);
+ $handler->updateTranslations();
+ $handler->save();
+ }
+}
+
+/**
+ * Implement hook_field_attach_delete().
+ */
+function entity_translation_field_attach_delete($obj_type, $object) {
+ if (entity_translation_entity_type_enabled($obj_type)) {
+ $handler = entity_translation_get_handler($obj_type, $object);
+ $handler->clearTranslations();
+ $handler->save();
+ }
+}
+
+/**
+ * Implement hook_form_alter().
+ */
+function entity_translation_form_alter(&$form, $form_state) {
+ $info = entity_translation_edit_form_info($form);
+
+ if ($info) {
+ $handler = entity_translation_get_handler($info['entity type'], $info['entity']);
+ $translations = $handler->getTranslations();
+
+ if (!empty($translations->data)) {
+ $form['entity_translation'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Translation'),
+ '#collapsible' => TRUE,
+ '#tree' => TRUE,
+ );
+
+ $form['entity_translation']['retranslate'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Flag translations as outdated'),
+ '#default_value' => 0,
+ '#description' => t('If you made a significant change, which means translations should be updated, you can flag all translations of this post as outdated. This will not change any other property of those posts, like whether they are published or not.'),
+ );
+
+ array_unshift($form['#submit'], 'entity_translation_edit_form_submit');
+ }
+
+ // Node specific alterations.
+ entity_translation_node_form_alter($form, $form_state, $handler);
+ }
+}
+
+/**
+ * Submit handler for the entity edit form.
+ *
+ * Mark translations as outdated if the submitted value is true.
+ */
+function entity_translation_edit_form_submit($form, &$form_state) {
+ $info = entity_translation_edit_form_info($form);
+ $handler = entity_translation_get_handler($info['entity type'], $info['entity']);
+ $outdated = (bool) $form_state['values']['entity_translation']['retranslate'];
+ $handler->setOutdated($outdated);
+}
+
+/**
+ * Entity translation handler factory.
+ *
+ * @param $entity_type
+ * The type of $entity; e.g. 'node' or 'user'.
+ * @param $entity
+ * The entity to be translated.
+ * @param $update
+ * Instances are statically cached: if this is TRUE the wrapped entity will
+ * be replaced by the passed one.
+ *
+ * @return
+ * A class implementing the EntityTranslationHandler interface.
+ */
+function entity_translation_get_handler($entity_type, $entity, $update = FALSE) {
+ $handlers =& drupal_static(__FUNCTION__);
+ list($entity_id) = entity_extract_ids($entity_type, $entity);
+
+ if (empty($entity_id)) {
+ $update = TRUE;
+ }
+
+ if (!isset($handlers[$entity_type][$entity_id])) {
+ $entity_info = entity_get_info($entity_type);
+ $class = $entity_info['translation']['entity_translation']['class'];
+ $handlers[$entity_type][$entity_id] = new $class($entity_type, $entity_info, $entity, $entity_id);
+ }
+ else if ($update) {
+ $handlers[$entity_type][$entity_id]->setEntity($entity);
+ }
+
+ return $handlers[$entity_type][$entity_id];
+}
+
+/**
+ * Return an array of edit form info as defined in hook_translation_info().
+ *
+ * @param $form
+ * The entity edit form.
+ *
+ * @return
+ * An edit form info array containing the entity to be translated in the
+ * 'entity' key.
+ */
+function entity_translation_edit_form_info($form) {
+ $edit_form_info = variable_get('entity_translation_edit_form_info', array());
+
+ if (isset($edit_form_info[$form['#id']])) {
+ $info = $edit_form_info[$form['#id']];
+ if (isset($form[$info['entity key']])) {
+ $info['entity'] = (object) $form[$info['entity key']];
+ return $info;
+ }
+ }
+
+ return FALSE;
+}
+
+/**
+ * Check if an entity translation is accessible.
+ *
+ * @param $translation
+ * An array representing an entity translation.
+ *
+ * @return
+ * TRUE if the current user is allowed to view the translation.
+ */
+function entity_translation_access($entity_type, $translation) {
+ return $translation['status'] || user_access('entity translation') || user_access('translate ' . $entity_type . ' entities');
+}
Index: modules/entity_translation/entity_translation.api.php
===================================================================
RCS file: modules/entity_translation/entity_translation.api.php
diff -N modules/entity_translation/entity_translation.api.php
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.api.php 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,53 @@
+ array(
+ * 'translation' => array(
+ * 'entity_translation' => array(
+ * 'class' => the entity class name,
+ * 'path' => the base menu path to which attach the administration UI,
+ * 'access callback' => the access callback for the admin pages,
+ * 'access arguments' => the access arguments,
+ * // The edit form information, used to add the retranslate checkbox
+ * // to the entity edit form.
+ * 'edit form' => array(
+ * 'form id' => the form id,
+ * 'entity key' => the key in hich the entity object is stored,
+ * ),
+ * ),
+ * ),
+ * ),
+ * );
+ */
+function hook_translation_info($types = NULL) {
+ $info = array();
+
+ $info['custom_entity'] = array(
+ 'translation' => array(
+ 'entity_translation' => array(
+ 'class' => 'EntityTranslationCustomEntityHandler',
+ 'path' => 'custom_entity/%custom_entity',
+ 'access callback' => 'entity_translation_custom_entity_tab_access',
+ 'access arguments' => array(1),
+ 'edit form' => array(
+ 'form id' => 'custom-entity-form',
+ 'entity key' => '#custom_entity',
+ ),
+ ),
+ ),
+ );
+
+ return $info;
+}
Index: modules/entity_translation/entity_translation.admin.inc
===================================================================
RCS file: modules/entity_translation/entity_translation.admin.inc
diff -N modules/entity_translation/entity_translation.admin.inc
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.admin.inc 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,416 @@
+ $info) {
+ if ($info['fieldable']) {
+ $options[$entity_type] = $info['label'];
+ }
+ }
+
+ $form['entity_translation_entity_types'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Translatable entity types'),
+ '#description' => t('Select which entities can be translated.'),
+ '#options' => $options,
+ '#default_value' => variable_get('entity_translation_entity_types', array()),
+ );
+
+ $form = system_settings_form($form);
+
+ // Menu rebuilding needs to be performed after the system settings are saved.
+ $form['#submit'][] = 'entity_translation_admin_form_submit';
+
+ return $form;
+}
+
+/**
+ * Submit handler for the entity translation settings form.
+ */
+function entity_translation_admin_form_submit($form, $form_state) {
+ menu_rebuild();
+}
+
+/**
+ * Translations overview menu callback.
+ */
+function entity_translation_overview($entity_type, $entity) {
+ $handler = entity_translation_get_handler($entity_type, $entity);
+
+ // Initialize translations if they are empty.
+ $translations = $handler->getTranslations();
+ if (empty($translations->original)) {
+ $handler->initTranslations();
+ $handler->save();
+ }
+
+ // Ensure that we have a coherent status between entity language and field
+ // languages.
+ if ($handler->initOriginalTranslation()) {
+ field_attach_update($entity_type, $entity);
+ }
+
+ $header = array(t('Language'), t('Source language'), t('Title'), t('Status'), t('Operations'));
+ // @todo: Do we want only enabled languages here?
+ $languages = language_list();
+ $source = isset($_SESSION['entity_translation_source_language']) ? $_SESSION['entity_translation_source_language'] : $translations->original;
+ $basePath = $handler->getBasePath();
+ $title = $handler->getHumanReadableId();
+ $path = $handler->getViewPath();
+
+ if ($path) {
+ // If we have a view path defined for the current entity get the switch
+ // links based on it.
+ $links = language_negotiation_get_switch_links(LANGUAGE_TYPE_CONTENT, $path);
+ }
+
+ foreach ($languages as $language) {
+ $options = array();
+ $language_name = $language->name;
+ $langcode = $language->language;
+
+ if (isset($translations->data[$langcode])) {
+ list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+
+ // Existing translation in the translation set: display status.
+ $is_original = $langcode == $translations->original;
+ $translation = $translations->data[$langcode];
+ $link = isset($links->links[$langcode]) ? $links->links[$langcode] : array();
+
+ if ($is_original) {
+ if (!empty($link)) {
+ $rowTitle = l($title, $link['href'], $link);
+ }
+ else {
+ $rowTitle = $path ? l($title, $path, array('language' => $language)) : $title;
+ }
+ }
+ else if (!empty($link)) {
+ $rowTitle = l(t('view'), $link['href'], $link);
+ }
+ else {
+ $rowTitle = t('n/a');
+ }
+
+ $path = "$basePath/entity-translation/translate/{$translations->original}/$langcode";
+ $editPath = $is_original ? $handler->getEditPath() : $path;
+ if ($editPath && $handler->getAccess('update')) {
+ $options[] = l(t('edit'), $editPath);
+ }
+
+ $status = $translation['status'] ? t('Published') : t('Not published');
+ $status .= isset($translation['translate']) && $translation['translate'] ? ' - ' . t('outdated') . '' : '';
+
+ if ($is_original) {
+ $language_name = t('@language_name ', array('@language_name' => $language_name));
+ $source_name = t('(original content)');
+ }
+ else {
+ $source_name = $languages[$translation['source']]->name;
+ }
+ }
+ else {
+ // No such translation in the set yet: help user to create it.
+ $rowTitle = $source_name = t('n/a');
+ $path = "$basePath/entity-translation/translate/$source/$langcode";
+
+ if ($source != $langcode && $handler->getAccess('update')) {
+ list(, , $bundle) = entity_extract_ids($entity_type, $entity);
+ $translatable = FALSE;
+
+ foreach (field_info_instances($entity_type, $bundle) as $instance) {
+ $field_name = $instance['field_name'];
+ $field = field_info_field($field_name);
+ if ($field['translatable']) {
+ $translatable = TRUE;
+ break;
+ }
+ }
+
+ $options[] = $translatable ? l(t('add translation'), $path) : t('No translatable fields');
+ }
+ $status = t('Not translated');
+ }
+ $rows[] = array($language_name, $source_name, $rowTitle, $status, implode(" | ", $options));
+ }
+
+ drupal_set_title(t('Translations of %title', array('%title' => $title)), PASS_THROUGH);
+
+ $build['entity_translation_overview'] = array(
+ '#theme' => 'table',
+ '#header' => $header,
+ '#rows' => $rows,
+ );
+
+ return $build;
+}
+
+/**
+ * Translation adding/editing form.
+ */
+function entity_translation_translation_form($form, $form_state, $entity_type, $entity, $source, $langcode) {
+ $handler = entity_translation_get_handler($entity_type, $entity);
+
+ $languages = language_list();
+ $args = array('@title' => $handler->getHumanReadableId(), '@language' => t($languages[$langcode]->name));
+ drupal_set_title(t('@title [@language translation]', $args));
+
+ $translations = $handler->getTranslations();
+ $new_translation = !isset($translations->data[$langcode]);
+
+ $form = array(
+ '#handler' => $handler,
+ '#entity_type' => $entity_type,
+ '#entity' => $entity,
+ '#source' => $source,
+ '#language' => $langcode,
+ );
+
+ // Display source language selector only if we are creating a new translation
+ // and there are at least two translations available.
+ if ($new_translation && count($translations->data) > 1) {
+ $form['source_language'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Source language'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#tree' => TRUE,
+ '#weight' => -22,
+ 'language' => array(
+ '#type' => 'select',
+ '#default_value' => $source,
+ '#options' => array(),
+ ),
+ 'submit' => array(
+ '#type' => 'submit',
+ '#value' => t('Change'),
+ '#submit' => array('entity_translation_translation_form_source_language_submit'),
+ ),
+ );
+ foreach (language_list() as $language) {
+ if (isset($translations->data[$language->language])) {
+ $form['source_language']['language']['#options'][$language->language] = t($language->name);
+ }
+ }
+ }
+
+ $translate = intval(isset($translations->data[$langcode]) && $translations->data[$langcode]['translate']);
+
+ $form['translation'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Translation settings'),
+ '#collapsible' => TRUE,
+ '#collapsed' => !$translate,
+ '#tree' => TRUE,
+ '#weight' => -24,
+ );
+ $form['translation']['status'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('This translation is published'),
+ '#default_value' => isset($translations->data[$langcode]) && $translations->data[$langcode]['status'],
+ '#description' => t('When this option is unchecked, this translation will not be visible for non-administrators.'),
+ );
+ $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 because the source post has changed. Uncheck when the translation is up to date again.'),
+ '#disabled' => !$translate,
+ );
+
+ // If we are creating a new translation we need to retrieve form elements
+ // populated with the source language values.
+ $field_view = field_attach_view($entity_type, $entity, 'full', $langcode);
+ $source_form = array();
+ if ($new_translation) {
+ $source_form_state = $form_state;
+ field_attach_form($entity_type, $entity, $source_form, $source_form_state, $source);
+ }
+ field_attach_form($entity_type, $entity, $form, $form_state, $langcode);
+
+ list(, , $bundle) = entity_extract_ids($entity_type, $entity);
+
+ foreach (field_info_instances($entity_type, $bundle) as $instance) {
+ $field_name = $instance['field_name'];
+ $field = field_info_field($field_name);
+ // If a field is not translatable it should not be editable from the
+ // translation form, yet it could be useful to display its value.
+ if (!$field['translatable']) {
+ $form[$field_name] = array(
+ '#markup' => drupal_render($field_view[$field_name]),
+ // Place the element where it would appear if displayed.
+ '#weight' => $instance['display']['full']['weight'],
+ );
+ }
+ // If we are creating a new translation we have to change the form item
+ // language information from source to target language, this way the
+ // user can find the form items already populated with the source values
+ // while the field form element holds the correct language information.
+ else if ($new_translation && !isset($entity->{$field_name}[$langcode]) && isset($source_form[$field_name][$source])) {
+ $form[$field_name][$langcode] = $source_form[$field_name][$source];
+ }
+ }
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save translation'),
+ '#submit' => array('entity_translation_translation_form_save_submit'),
+ );
+
+ if (!$new_translation) {
+ $form['delete'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete translation'),
+ '#submit' => array('entity_translation_translation_form_delete_submit'),
+ );
+ }
+
+ // URL alias widget.
+ if (_entity_translation_path_enabled($handler)) {
+ $alias = db_select('url_alias')
+ ->fields('url_alias', array('alias'))
+ ->condition('source', $handler->getViewPath())
+ ->condition('language', $langcode)
+ ->execute()
+ ->fetchField();
+
+ $form['path'] = array(
+ '#type' => 'textfield',
+ '#title' => t('URL alias'),
+ '#default_value' => $alias,
+ '#maxlength' => 255,
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#description' => t('Optionally specify an alternative URL by which this entity can be accessed. For example, type "about" when writing an about page. Use a relative path and don\'t add a trailing slash or the URL alias won\'t work.'),
+ '#access' => user_access('create url aliases'),
+ '#weight' => -20,
+ );
+
+ if (!empty($alias)) {
+ $pid = db_select('url_alias')
+ ->fields('url_alias', array('pid'))
+ ->condition('alias', $alias)
+ ->condition('language', $langcode)
+ ->execute()
+ ->fetchField();
+
+ $form['path']['pid'] = array(
+ '#type' => 'value',
+ '#value' => $pid,
+ );
+ }
+ }
+
+ return $form;
+}
+
+/**
+ * Submit handler for the source language selector.
+ */
+function entity_translation_translation_form_source_language_submit($form, &$form_state) {
+ $handler = $form['#handler'];
+ $langcode = $form_state['values']['source_language']['language'];
+ $path = "{$handler->getBasePath()}/entity-translation/translate/$langcode/{$form['#language']}";
+ $form_state['redirect'] = array('path' => $path);
+ $languages = language_list();
+ drupal_set_message(t('Source translation set to: %language', array('%language' => t($languages[$langcode]->name))));
+}
+
+/**
+ * Submit handler for the translation saving.
+ */
+function entity_translation_translation_form_save_submit($form, &$form_state) {
+ $handler = $form['#handler'];
+
+ $translation = array(
+ 'translate' => $form_state['values']['translation']['translate'],
+ 'status' => $form_state['values']['translation']['status'],
+ 'language' => $form['#language'],
+ 'source' => $form['#source'],
+ );
+
+ $handler->setTranslation($translation, $form_state['values']);
+ field_attach_update($form['#entity_type'], $form['#entity']);
+
+ // Update URL alias.
+ if (_entity_translation_path_enabled($handler) && (user_access('create url aliases') || user_access('administer url aliases'))) {
+ $path = array(
+ 'source' => $handler->getViewPath(),
+ 'alias' => $form_state['values']['path'],
+ 'pid' => isset($form_state['values']['pid']) ? $form_state['values']['pid'] : NULL,
+ 'language' => $form['#language'],
+ );
+ if (!empty($path['pid']) && empty($path['alias'])) {
+ path_delete($path['pid']);
+ }
+ if (!empty($path['alias'])) {
+ path_save($path);
+ }
+ }
+
+ $form_state['redirect'] = "{$handler->getBasePath()}/entity-translation";
+}
+
+/**
+ * Helper function to check if the path support is enabled.
+ */
+function _entity_translation_path_enabled(EntityTranslationHandlerInterface $handler) {
+ return $handler->isAliasEnabled() && module_exists('path');
+}
+
+/**
+ * Submit handler for the translation deletion.
+ */
+function entity_translation_translation_form_delete_submit($form, &$form_state) {
+ $form_state['redirect'] = "{$form['#handler']->getBasePath()}/entity-translation/delete/{$form['#source']}/{$form['#language']}";
+}
+
+/**
+ * Translation deletion confirmation form.
+ */
+function entity_translation_translation_delete_confirm($form, $form_state, $entity_type, $entity, $source, $langcode) {
+ $handler = entity_translation_get_handler($entity_type, $entity);
+ $languages = language_list();
+
+ $form = array(
+ '#handler' => $handler,
+ '#entity_type' => $entity_type,
+ '#entity' => $entity,
+ '#language' => $langcode,
+ );
+
+ return confirm_form($form,
+ t('Are you sure you want to delete the @language translation of %title?',
+ array('@language' => $languages[$langcode]->name, '%title' => $handler->getHumanReadableId())),
+ "{$handler->getBasePath()}/entity-translation/translate/$source/$langcode",
+ t('This action cannot be undone.'),
+ t('Delete'),
+ t('Cancel')
+ );
+}
+
+/**
+ * Submit handler for the translation deletion confirmation.
+ */
+function entity_translation_translation_delete_confirm_submit($form, &$form_state) {
+ $handler = $form['#handler'];
+
+ $handler->removeTranslation($form['#language']);
+ field_attach_update($form['#entity_type'], $form['#entity']);
+
+ if (isset($_SESSION['entity_translation_source_language']) && $form['#language'] == $_SESSION['entity_translation_source_language']) {
+ unset($_SESSION['entity_translation_source_language']);
+ }
+
+ $form_state['redirect'] = "{$handler->getBasePath()}/entity-translation";
+}
Index: modules/entity_translation/modules/entity_translation_upgrade/entity_translation_upgrade.info
===================================================================
RCS file: modules/entity_translation/modules/entity_translation_upgrade/entity_translation_upgrade.info
diff -N modules/entity_translation/modules/entity_translation_upgrade/entity_translation_upgrade.info
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/modules/entity_translation_upgrade/entity_translation_upgrade.info 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,9 @@
+; $Id$
+name = Entity translation Upgrade
+description = Provide an upgrade path from the node-based translation.
+dependencies[] = entity_translation
+package = Core
+version = VERSION
+core = 7.x
+files[] = entity_translation_upgrade.install
+files[] = entity_translation_upgrade.module
Index: modules/entity_translation/entity_translation.node.inc
===================================================================
RCS file: modules/entity_translation/entity_translation.node.inc
diff -N modules/entity_translation/entity_translation.node.inc
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.node.inc 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,180 @@
+getTranslations();
+
+ // Disable languages for existing translations, so it is not possible to switch this
+ // node to some language which is already in the translation set.
+ foreach ($translations->data as $langcode => $translation) {
+ if ($langcode != $translations->original) {
+ unset($form['language']['#options'][$langcode]);
+ }
+ }
+ if (count($translations->data) > 1) {
+ unset($form['language']['#options']['']);
+ }
+
+ if (isset($form['entity_translation'])) {
+ $form['entity_translation'] += array(
+ '#group' => 'additional_settings',
+ '#weight' => 100,
+ '#attached' => array(
+ 'js' => array(drupal_get_path('module', 'entity_translation') . '/entity-translation-node-form.js'),
+ ),
+ );
+ }
+}
+
+/**
+ * Implement hook_node_view().
+ *
+ * Provide content language switcher links to navigate among node translations.
+ */
+function entity_translation_node_view($node, $build_mode) {
+ if (!empty($node->translations) && drupal_multilingual()) {
+ global $language;
+ $path = 'node/' . $node->nid;
+ $links = language_negotiation_get_switch_links(LANGUAGE_TYPE_CONTENT, $path);
+
+ if (is_object($links) && !empty($links->links)) {
+ $handler = entity_translation_get_handler('node', $node);
+ $translations = $handler->getTranslations()->data;
+
+ // Remove the link for the current language.
+ unset($links->links[$language->language]);
+
+ // Remove links to unavailable translations.
+ foreach ($links->links as $langcode => $link) {
+ if (!isset($translations[$langcode]) || !entity_translation_access('node', $translations[$langcode])) {
+ unset($links->links[$langcode]);
+ }
+ }
+
+ $node->content['links']['translation'] = array(
+ '#theme' => 'links',
+ '#links' => $links->links,
+ '#attributes' => array('class' => 'links inline'),
+ );
+ }
+ }
+}
+
+/**
+ * Implement hook_form_FORM_ID_alter().
+ *
+ * Provide settings into the node content type form to choose for entity
+ * translation metadata and comment filtering.
+ */
+function entity_translation_form_node_type_form_alter(&$form, &$form_state) {
+ $type = $form['#node_type']->type;
+
+ $form['display']['entity_translation_node_metadata'] = array(
+ '#type' => 'radios',
+ '#title' => t('Translation post information'),
+ '#default_value' => variable_get("entity_translation_node_metadata_$type", ENTITY_TRANSLATION_METADATA_HIDE),
+ '#options' => array(t('Hidden'), t('Shown'), t('Replacing post information')),
+ );
+
+ if (isset($form['comment'])) {
+ $form['comment']['entity_translation_comment_filter'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Filter comments per language'),
+ '#default_value' => variable_get("entity_translation_comment_filter_$type", FALSE),
+ '#description' => t('Show only comments whose language matches content language.'),
+ );
+ }
+}
+
+/**
+ * Implement hook_preprocess_node().
+ *
+ * Alter node template variables to show/replace entity translation metadata.
+ */
+function entity_translation_preprocess_node(&$variables) {
+ $node = $variables['node'];
+ $submitted = variable_get("node_submitted_{$node->type}", TRUE);
+ $mode = variable_get("entity_translation_node_metadata_{$node->type}", ENTITY_TRANSLATION_METADATA_HIDE);
+
+ if ($submitted && $mode != ENTITY_TRANSLATION_METADATA_HIDE) {
+ global $language, $user;
+
+ $handler = entity_translation_get_handler('node', $node);
+ $translations = $handler->getTranslations();
+ $langcode = $language->language;
+
+ if (isset($translations->data[$langcode]) && $langcode != $translations->original) {
+ $translation = $translations->data[$langcode];
+ $date = format_date($translation['created']);
+ $name = FALSE;
+
+ if ($node->uid != $translation['uid']) {
+ $account = $user->uid != $translation['uid'] ? user_load($translation['uid']) : $user;
+ $name = theme('username', array('account' => $account));
+ }
+
+ switch ($mode) {
+ case ENTITY_TRANSLATION_METADATA_SHOW:
+ $variables['date'] .= ' (' . t('translated on !date', array('!date' => $date)) . ')';
+ if ($name) {
+ $variables['name'] .= ' (' . t('translated by !name', array('!name' => $name)) . ')';
+ }
+ break;
+
+ case ENTITY_TRANSLATION_METADATA_REPLACE:
+ $variables['date'] = $date;
+ if ($name) {
+ $variables['name'] = $name;
+ }
+ break;
+ }
+ }
+ }
+}
+
+/**
+ * Implement hook_query_TAG_alter().
+ *
+ * Filter out node comments by content language.
+ */
+function entity_translation_query_comment_filter_alter(QueryAlterableInterface $query) {
+ $node = $query->getMetaData('node');
+ if (variable_get("entity_translation_comment_filter_{$node->type}", FALSE)) {
+ global $language;
+ $query->where("language = :language OR language = ''", array(':language' => $language->language));
+ }
+}
+
+/**
+ * Node specific access callback.
+ */
+function entity_translation_node_tab_access($node) {
+ return !empty($node->language) && locale_multilingual_node_type($node->type) && entity_translation_tab_access('node');
+}
Index: modules/entity_translation/entity_translation.handler.node.inc
===================================================================
RCS file: modules/entity_translation/entity_translation.handler.node.inc
diff -N modules/entity_translation/entity_translation.handler.node.inc
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.handler.node.inc 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,34 @@
+entity->revision);
+ }
+
+ public function getLanguage() {
+ return $this->entity->language;
+ }
+
+ public function getHumanReadableId() {
+ return $this->entity->title[FIELD_LANGUAGE_NONE][0]['value'];
+ }
+
+ public function getAccess($op) {
+ return node_access($op, $this->entity);
+ }
+
+ protected function getStatus() {
+ return (boolean) $this->entity->status;
+ }
+}
Index: modules/entity_translation/modules/entity_translation_upgrade/entity_translation_upgrade.module
===================================================================
RCS file: modules/entity_translation/modules/entity_translation_upgrade/entity_translation_upgrade.module
diff -N modules/entity_translation/modules/entity_translation_upgrade/entity_translation_upgrade.module
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/modules/entity_translation_upgrade/entity_translation_upgrade.module 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,81 @@
+ array(
+ 'title' => 'Entity Translation Upgrade',
+ 'page callback' => 'entity_translation_upgrade_start',
+ 'access arguments' => array('administer software updates'),
+ 'file' => 'entity_translation_upgrade.install',
+ 'type' => MENU_CALLBACK,
+ ),
+ );
+}
+
+/**
+ * Implement hook_menu_alter().
+ */
+function entity_translation_upgrade_menu_alter(&$items) {
+ // Obsolete node translations might be left unpublished instead of being
+ // deleted.
+ $items['node/%node']['access callback'] = 'entity_translation_upgrade_access';
+ $items['node/%node']['access arguments'] = array(1);
+}
+
+/**
+ * Implement hook_init().
+ */
+function entity_translation_upgrade_init() {
+ if (arg(0) == 'node' && ($nid = arg(1)) && !node_load($nid)) {
+
+ // We look for a record matching the requested nid in the history table.
+ $data = db_select('entity_translation_upgrade_history', 'et')
+ ->fields('et')
+ ->condition('et.nid', $nid)
+ ->execute()
+ ->fetchObject();
+
+ if ($data) {
+ _entity_translation_upgrade_redirect($data->tnid, $data->language);
+ }
+ }
+}
+
+/**
+ * Access callback.
+ *
+ * Perform a redirect to the corresponding field-based translation if the
+ * current user has not the permission to access the requested node translation.
+ */
+function entity_translation_upgrade_access($node) {
+ // If the user has the right to access the node, we need to do nothing.
+ if (node_access('view', $node)) {
+ return TRUE;
+ }
+
+ // If we have a node translation, we need to redirect the user to the original
+ // node.
+ if ($node->tnid && $node->nid != $node->tnid) {
+ _entity_translation_upgrade_redirect($node->tnid, $node->language);
+ }
+
+ // Nothing to do again.
+ return FALSE;
+}
+
+/**
+ * Perform the redirect to original node with the given language.
+ */
+function _entity_translation_upgrade_redirect($nid, $langcode) {
+ $languages = language_list();
+ drupal_goto('node/' . $nid, array('language' => $languages[$langcode]), 301);
+}
Index: modules/entity_translation/entity_translation.install
===================================================================
RCS file: modules/entity_translation/entity_translation.install
diff -N modules/entity_translation/entity_translation.install
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.install 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,100 @@
+fields(array('weight' => 1))
+ ->condition('name', 'entity_translation')
+ ->execute();
+
+ // Enable translation for nodes.
+ variable_set('entity_translation_entity_types', array('node' => 'node'));
+}
+
+/**
+ * Implement hook_uninstall().
+ */
+function entity_translation_uninstall() {
+ variable_del('entity_translation_edit_form_info');
+ variable_del('entity_translation_entity_types');
+
+ foreach (node_type_get_types() as $type => $object) {
+ variable_del("entity_translation_node_metadata_$type");
+ variable_del("entity_translation_comment_filter_$type");
+ }
+}
+
+/**
+ * Implement hook_schema().
+ */
+function entity_translation_schema() {
+ $schema['entity_translation'] = array(
+ 'description' => 'Table to track entity translations',
+ 'fields' => array(
+ 'etid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'The entity type id this translation relates to',
+ ),
+ 'entity_id' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'The entity id this translation relates to',
+ ),
+ // @todo: Consider an integer field for 'language'.
+ 'language' => 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.',
+ ),
+ 'uid' => array(
+ 'description' => 'The author of this translation.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'status' => array(
+ 'description' => 'Boolean indicating whether the translation is published (visible to non-administrators).',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 1,
+ ),
+ 'translate' => array(
+ 'description' => 'A boolean indicating whether this translation needs to be updated.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'created' => array(
+ 'description' => 'The Unix timestamp when the translation was created.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'changed' => array(
+ 'description' => 'The Unix timestamp when the translation was most recently saved.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('etid', 'entity_id', 'language'),
+ );
+
+ return $schema;
+}