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,14 @@ +; $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.node.inc +files[] = entity_translation.taxonomy.inc +files[] = entity_translation.user.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,427 @@ +entityType = $entity_type; + $this->entityInfo = $entity_info; + $this->entity = $entity; + $this->entityId = $entity_id; + + $this->translating = FALSE; + $this->outdated = FALSE; + } + + public function load() { + $translations_key = $this->getTranslationsKey(); + $this->entity->{$translations_key} = $this->readTranslations(); + } + + public function save() { + $this->writeTranslations(); + } + + public function initTranslations() { + $langcode = $this->getLanguage(); + + if (!empty($langcode)) { + $translation = array('language' => $langcode, 'status' => $this->getStatus()); + $this->setTranslation($translation); + $this->setOriginalLanguage($langcode); + } + } + + public function fixOriginalTranslation() { + $fixed = FALSE; + $translations = $this->getTranslations(); + list(, , $bundle) = field_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 != $translations->original && $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 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) = field_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 = NULL) { + $translations_key = $this->getTranslationsKey(); + + if (!empty($langcode)) { + unset($this->entity->{$translations_key}->data[$langcode]); + } + else { + $this->entity->{$translations_key}->data = array(); + } + + list(, , $bundle) = field_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 getLanguage() { + return language_default()->language; + } + + public function setLanguage($langcode) { + $this->language = $langcode; + } + + public function isRevision() { + return FALSE; + } + + public function prepareRevision($langcode) { + $entity = clone($this->entity); + field_attach_load($this->entityType, array($this->getId() => $entity)); + list(, , $bundle) = field_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']) { + foreach ($entity->{$field_name} as $lang => $items) { + if ($lang != $langcode) { + $this->entity->{$field_name}[$lang] = $items; + } + } + } + } + } + + public function isTranslating() { + return $this->translating; + } + + public function setTranslating($translating) { + $this->translating = $translating; + } + + public function getOriginalLanguage() { + return $this->getTranslations()->original; + } + + 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 getHumanReadableId() { + return "{$this->entityType}:{$this->getId()}"; + } + + public function getAccess($op) { + return TRUE; + } + + /** + * 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 getId() { + 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); + } + + /** + * Read the translation data from the storage. + */ + protected function readTranslations() { + $etid = $this->getEtid(); + + $results = db_select('entity_translation', 'et') + ->fields('et') + ->condition('etid', $etid) + ->condition('entity_id', $this->entityId) + ->orderBy('created') + ->execute(); + + $translations = array(); + foreach ($results as $row) { + $translations[$row->language] = (array) $row; + // Only the original translation has an empty source. + if (empty($row->source)) { + $original = $row->language; + } + } + + return (object) array('original' => isset($original) ? $original : NULL, 'data' => $translations); + } + + /** + * 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(); + + global $user; + $translations = $this->getTranslations(); + + foreach ($translations->data as $langcode => $translation) { + $translation += array( + 'etid' => $etid, + 'entity_id' => $this->entityId, + 'source' => '', + 'uid' => $user->uid, + 'translate' => FALSE, + 'status' => FALSE, + 'created' => REQUEST_TIME, + 'changed' => REQUEST_TIME, + ); + + if ($this->outdated && $langcode != $translations->original) { + $translation['translate'] = TRUE; + } + + // @todo: Consider using a multi-insert query here. + drupal_write_record('entity_translation', $translation); + } + } +} 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-node', context).setSummary(function (context) { + return $('#edit-entity-translation-node-retranslate', context).is(':checked') ? + Drupal.t('Flag translations as outdated') : + Drupal.t('Don\'t flag translations as outdated'); + }); + } +}; + +})(jQuery); 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,360 @@ + array( + 'translation' => array( + 'entity_translation' => array( + 'class' => 'EntityTranslationNodeHandler', + 'path' => 'node/%node', + 'access callback' => 'entity_translation_node_tab_access', + 'access arguments' => array(1), + 'edit form' => array( + 'form id' => 'node-form', + 'entity key' => '#node', + ), + ), + ), + ), + 'taxonomy_term' => array( + 'translation' => array( + 'entity_translation' => array( + 'class' => 'EntityTranslationTaxonomyTermHandler', + 'path' => 'taxonomy/term/%taxonomy_term', + 'edit form' => array( + 'form id' => 'taxonomy-form-term', + 'entity key' => '#term', + ), + ), + ), + ), + 'user' => array( + 'translation' => array( + 'entity_translation' => array( + 'class' => 'EntityTranslationUserHandler', + 'path' => 'user/%user', + 'edit form' => array( + 'form id' => 'user-profile-form', + 'entity key' => '#user', + ), + ), + ), + ), + ); + + 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(); + + foreach (field_info_fieldable_types() as $entity_type => $info) { + // Provide defaults for translation info. + if (!isset($entity_info[$entity_type]['translation']['entity_translation'])) { + $entity_info[$entity_type]['translation']['entity_translation'] = array(); + } + $entity_info[$entity_type]['translation']['entity_translation'] += array( + 'access callback' => 'entity_translation_tab_access', + '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); +} + +/** + * Implement hook_menu(). + */ +function entity_translation_menu() { + $items = array(); + + // Create tabs for all possible bundles. + foreach (field_info_fieldable_types() as $entity_type => $info) { + if (isset($info['translation']['entity_translation']['path'])) { + // Extract informations from the bundle description. + $path = $info['translation']['entity_translation']['path']; + $keys = array('theme callback', 'theme arguments', 'access callback', 'access arguments'); + $item = array_intersect_key($info['translation']['entity_translation'], drupal_map_assoc($keys)); + $entity_position = count(explode('/', $path)) - 1; + + $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, $entity_position + 3, $entity_position + 4), + '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, $entity_position + 3, $entity_position + 4), + 'file' => 'entity_translation.admin.inc', + ) + $item; + } + } + + return $items; +} + +/** + * Access callback. + */ +function entity_translation_tab_access() { + return drupal_multilingual() && user_access('entity translation'); +} + +/** + * Menu loader callback. + */ +function entity_translation_language_load($langcode) { + $enabled_languages = field_multilingual_content_languages(); + return in_array($langcode, $enabled_languages) ? $langcode : FALSE; +} + +/** + * Implement hook_permission(). + */ +function entity_translation_permission() { + return array( + 'entity translation' => array( + 'title' => t('Translate entities'), + 'description' => t('Translate field content for any fieldable entity.'), + ), + ); +} + +/** + * Implement hook_field_attach_view_alter(). + */ +function entity_translation_field_attach_view_alter(&$output, $context) { + global $language; + + $handler = entity_translation_get_handler($context['obj_type'], $context['object']); + $translations = $handler->getTranslations(); + $langcode = $language->language; + + if (!isset($translations->data[$langcode]) && !variable_get('locale_field_fallback_view', TRUE)) { + drupal_not_found(); + exit; + } + + $translation = isset($translations->data[$langcode]) ? $translations->data[$langcode] : FALSE; + if ($translation && !entity_translation_access($translation)) { + drupal_access_denied(); + exit; + } +} + +/** + * Implement hook_field_attach_pre_insert(). + */ +function entity_translation_field_attach_pre_insert($obj_type, $object) { + $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) { + $handler = entity_translation_get_handler($obj_type, $object, TRUE); + $langcode = $handler->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($handler->getTranslations()->data)) { + $handler->initTranslations(); + } + elseif (!empty($langcode) && !$handler->isTranslating()) { + $handler->setOriginalLanguage($langcode); + } + + if ($handler->isRevision()) { + $handler->prepareRevision($langcode); + } + + $handler->save(); +} + +/** + * Implement hook_field_attach_delete(). + */ +function entity_translation_field_attach_delete($obj_type, $object) { + $handler = entity_translation_get_handler($obj_type, $object); + $handler->removeTranslation(); + $handler->save(); +} + + +/* Forms */ + +/** + * 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'); + } + + $function = "entity_translation_{$info['entity type']}_form_alter"; + if (function_exists($function)) { + $function($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); +} + + +/* Helper functions */ + +/** + * 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 entitie will + * be replaced by the passed ones. + * + * @return + * A class implementing the EntityTranslationHandler interface. + */ +function entity_translation_get_handler($entity_type, $entity, $update = FALSE) { + $handlers =& drupal_static(__FUNCTION__); + list($entity_id) = field_extract_ids($entity_type, $entity); + + 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($translation) { + return $translation['status'] || user_access('entity translation'); +} Index: modules/entity_translation/entity_translation.user.inc =================================================================== RCS file: modules/entity_translation/entity_translation.user.inc diff -N modules/entity_translation/entity_translation.user.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/entity_translation/entity_translation.user.inc 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,31 @@ +getId()}"; + } + + public function getEditPath() { + return "user/{$this->getId()}/edit"; + } + + public function getHumanReadableId() { + return $this->entity->name; + } +} 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,308 @@ +getTranslations(); + if (empty($translations->original)) { + $handler->initTranslations(); + $handler->save(); + } + + // Ensure that we have a coherent status between entity language and field + // languages. + if ($handler->fixOriginalTranslation()) { + field_attach_update($entity_type, $entity); + } + + $header = array(t('Language'), t('Source language'), t('Title'), t('Status'), t('Operations')); + $languages = language_list(); + $source = isset($_SESSION['entity_translation_source_language']) ? $_SESSION['entity_translation_source_language'] : $translations->original; + $basePath = $handler->getBasePath(); + $title = $handler->getHumanReadableId(); + + foreach ($languages as $language) { + $options = array(); + $language_name = $language->name; + $langcode = $language->language; + + if (isset($translations->data[$langcode])) { + list($id, $vid, $bundle) = field_extract_ids($entity_type, $entity); + + // Existing translation in the translation set: display status. + $is_original = $langcode == $translations->original; + $translation = $translations->data[$langcode]; + $path = "$basePath/entity-translation/translate/{$translations->original}/$langcode"; + $rowTitle = l($is_original ? $title : t('view'), $basePath, array('language' => $language)); + + if ($handler->getAccess('update')) { + $options[] = l(t('edit'), $is_original ? $handler->getEditPath() : $path); + } + + $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) = field_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['translation_node_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' => -4, + '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 = isset($translations->data[$langcode]) && $translations->data[$langcode]['translate']; + + $form['translation'] = array( + '#type' => 'fieldset', + '#title' => t('Translation settings'), + '#collapsible' => TRUE, + '#collapsed' => !$translate, + '#tree' => TRUE, + '#weight' => -4, + ); + $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) = field_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['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'), + ); + } + + foreach (module_invoke_all('translation_info') as $type => $info) { + $function = "entity_translation_translation_{$type}_form_alter"; + if (function_exists($function)) { + $function($form, $form_state, $handler); + } + } + + 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']); + $handler->save(); + + // Save field translations. + field_attach_update($form['#entity_type'], $form['#entity']); + + $form_state['redirect'] = "{$handler->getBasePath()}/entity-translation"; +} + +/** + * 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/entity_translation.taxonomy.inc =================================================================== RCS file: modules/entity_translation/entity_translation.taxonomy.inc diff -N modules/entity_translation/entity_translation.taxonomy.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/entity_translation/entity_translation.taxonomy.inc 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,31 @@ +getId()}"; + } + + public function getEditPath() { + return "taxonomy/term/{$this->getId()}/edit"; + } + + public function getHumanReadableId() { + return $this->entity->name; + } +} 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,181 @@ +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_load(). + */ +function entity_translation_node_load($nodes) { + // @todo: Multiple load. + foreach ($nodes as $node) { + entity_translation_get_handler('node', $node)->load(); + } +} + +/** + * 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()) { + $path = 'node/' . $node->nid; + $links = language_negotiation_get_switch_links(LANGUAGE_TYPE_CONTENT, $path); + + if (is_object($links)) { + $links = $links->links; + $translations = $node->translations->data; + $languages = language_list('enabled'); + global $language; + + foreach ($languages[1] as $langcode => $target_language) { + if (!isset($translations[$langcode]) || $langcode == $language->language || !entity_translation_access($translations[$langcode])) { + unset($links[$langcode]); + } + } + + if (!empty($links)) { + $node->content['links']['translation'] = array( + '#theme' => 'links', + '#links' => $links, + '#attributes' => array('class' => 'links inline'), + ); + } + } + } +} + +/** + * Alter the translation form to create per language URL aliases. + */ +function entity_translation_translation_node_form_alter(&$form, $form_state) { + if (module_exists('path')) { + $node = $form['#entity']; + $langcode = $form['#language']; + + $path = db_query("SELECT dst FROM {url_alias} WHERE src = :src AND language = :language", array( + ':src' => 'node/' . $node->nid, + ':language' => $langcode, + ))->fetchField(); + + $form['path'] = array( + '#type' => 'textfield', + '#title' => t('URL alias'), + '#default_value' => $path, + '#maxlength' => 255, + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#description' => t('Optionally specify an alternative URL by which this node 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' => -3, + ); + + if ($path) { + $form['path']['pid'] = array( + '#type' => 'value', + '#value' => db_query("SELECT pid FROM {url_alias} WHERE dst = :dst AND language = :language", array( + ':dst' => $path, + ':language' => $langcode, + )) + ->fetchField(), + ); + } + + $form['submit']['#submit'][] = 'entity_translation_translation_node_form_save_submit'; + } +} + +/** + * Submit handler for the node translation form. + */ +function entity_translation_translation_node_form_save_submit($form, &$form_state) { + if (user_access('create url aliases') || user_access('administer url aliases')) { + $node = $form['#entity']; + $language = $form['#language']; + $path = $form_state['values']['path']; + $pid = $form_state['values']['pid']; + path_set_alias('node/' . $node->nid, !empty($path) ? $path : NULL, !empty($pid) ? $pid : NULL, $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 translation handler. + * + * Override the default behaviours to provide the needed node properties. + */ +class EntityTranslationNodeHandler extends EntityTranslationDefaultHandler { + + public function __construct($entity_type, $entity_info, $entity, $entity_id) { + parent::__construct('node', $entity_info, $entity, $entity_id); + } + + public function isRevision() { + return !empty($this->entity->revision); + } + + public function getLanguage() { + return $this->entity->language; + } + + public function getBasePath() { + return "node/{$this->getId()}"; + } + + public function getEditPath() { + return "node/{$this->getId()}/edit"; + } + + 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/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,84 @@ +fields(array('weight' => 1)) + ->condition('name', 'entity_translation') + ->execute(); +} + +/** + * 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; +}