'.t('This module synchronizes content taxonomy and fields accross translations:').'

'; $output .= '

'.t('First you need to select which fields should be synchronized. Then, after a node has been updated, all enabled vocabularies and fields will be synchronized as follows:').'

'; $output .= ''; $output .= '

'.t('Note that permissions are not checked for each node so if someone can edit a node and it is set to synchronize, all the translations will be synchronized anyway.').'

'; $output .= '

'.t('To enable synchronization check content type options to select which fields to synchronize for each node type').'

'; $output .= '

'.t('The list of available fields for synchronization will include some standard node fields and all CCK fields. You can add more fields to the list in a configuration variable. See README.txt for how to do it.').'

'; $output .= '

'. t('For more information please read the on-line help pages.', array('@i18n' =>'http://drupal.org/node/31631')) .'

'; return $output; } } /** * Implementation of hook_theme() */ function i18nsync_theme() { return array( 'i18nsync_workflow_checkbox' => array( 'arguments' => array('item' => NULL), ), ); } /** * Implementation of hook_form_alter(). * - Vocabulary options * - Content type options */ function i18nsync_form_alter(&$form, $form_state, $form_id) { // Taxonomy vocabulary form switch ($form_id) { case 'node_type_form': $type = $form['#node_type']->type; $current = i18nsync_node_fields($type); $form['workflow']['i18n']['i18nsync_nodeapi'] = array( '#type' => 'fieldset', '#tree' => TRUE, '#title' => t('Synchronize translations'), '#collapsible' => TRUE, '#collapsed' => !count($current), '#description' => t('Select which fields to synchronize for all translations of this content type.') ); // Each set provides title and options. We build a big checkboxes control for it to be // saved as an array. Special themeing for group titles. foreach (i18nsync_node_available_fields($type) as $group => $data) { $title = $data['#title']; foreach ($data['#options'] as $field => $name) { $form['workflow']['i18n']['i18nsync_nodeapi'][$field] = array( '#group_title' => $title, '#title' => $name, '#type' => 'checkbox', '#default_value' => in_array($field, $current), '#theme' => 'i18nsync_workflow_checkbox', ); $title = ''; } } break; } } /** * Theming function for workflow checkboxes */ function theme_i18nsync_workflow_checkbox($element){ $output = $element['#group_title'] ? '
'.$element['#group_title'].'
' : ''; $output .= theme('checkbox', $element); return $output; } /** * Implementation of hook_nodeapi(). */ function i18nsync_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) { global $i18nsync; // This variable will be true when a sync operation is in progress // Only for nodes that have language and belong to a translation set. if (translation_supported_type($node->type) && !empty($node->language) && !$i18nsync) { switch ($op) { case 'prepare translation': // We copy over all the fields to be synchronized if ($fields = i18nsync_node_fields($node->type)) { i18nsync_prepare_translation($node, $node->translation_source, $fields); } break; case 'insert': // When creating a translation we need to save the files that have been inherited if (!empty($node->translation_source) && !empty($node->files)) { foreach ($node->files as $fid => $file) { $file = (object)$file; if (empty($file->remove) && !isset($_SESSION['upload_files'][$fid])) { db_query("INSERT INTO {upload} (fid, nid, vid, list, description, weight) VALUES (%d, %d, %d, %d, '%s', %d)", $file->fid, $node->nid, $node->vid, $file->list, $file->description, $file->weight); } } } // Intentional no break case 'update': // Let's go with field synchronization. if (!empty($node->tnid) && ($fields = i18nsync_node_fields($node->type)) && ($translations = translation_node_get_translations($node->tnid)) && count($translations) > 1) { $i18nsync = TRUE; foreach ($translations as $trnode) { if ($node->nid != $trnode->nid) { i18nsync_node_translation($node, $trnode, $fields, $op); } } $i18nsync = FALSE; drupal_set_message(t('All %count node translations have been synchronized', array('%count' => count($translations) - 1))); } break; } } } /** * Prepare node translation. Copy over sincronizable fields */ function i18nsync_prepare_translation(&$node, $source, $field_list){ foreach ($field_list as $field) { if (empty($source->$field)) continue; switch($field) { case 'taxonomy': i18nsync_taxonomyfield($node, $source); break; default: $node->$field = $source->$field; break; } } } /** * Synchronizes fields for node translation * * There's some specific handling for known fields like: * - files, for file attachments * - iid (CCK node attachments, translations for them will be handled too) * * All the rest of the fields will be just copied over. * The 'revision' field will have the special effect of creating a revision too for the translation * * @param $node * Source node being edited * @param $translation * Node translation to synchronize, just needs nid property * @param $fields * List of fields to synchronize * @param $op * Node operation (insert|update) */ function i18nsync_node_translation($node, $translation, $fields, $op) { // Load full node, we need all data here $translation = node_load($translation->nid); foreach ($fields as $field) { switch($field) { case 'taxonomy': // Do nothing it has already been syncd i18nsync_taxonomyfield($translation, $node); break; case 'parent': // Book outlines, translating parent page if exists case 'iid': // Attached image nodes i18nsync_node_translation_attached_node($node, $translation, $field); break; case 'files': // Sync existing attached files foreach ($node->files as $fid => $file) { if (isset($translation->files[$fid])) { $translation->files[$fid]->list = $file->list; } else { // New file. Create new revision of file for the translation $translation->files[$fid] = $file; // If it's a new node revision it will just be created, but if it's not // we have to update the table directly. The revision field was before this one in the list if (!isset($translation->revision) || !$translation->revision) { db_query("INSERT INTO {upload} (fid, nid, vid, list, description) VALUES (%d, %d, %d, %d, '%s')", $file->fid, $translation->nid, $translation->vid, $file->list, $file->description); } } } // Drop removed files foreach ($translation->files as $fid => $file) { if (!isset($node->files[$fid])) { $translation->files[$fid]->remove = TRUE; } } break; default: // For fields that don't need special handling if (isset($node->$field)) { $translation->$field = $node->$field; } } } node_save($translation); } /** * Synchronize taxonomy * * Translate translatable terms, just copy over the rest */ function i18nsync_taxonomyfield(&$node, $source) { if (module_exists('i18ntaxonomy')) { $node->taxonomy = i18ntaxonomy_translate_terms($source->taxonomy, $node->language); } else { // If not multilingual taxonomy enabled, just copy over $node->taxonomy = $source->taxonomy; } } /** * Node attachments (CCK) that may have translation */ function i18nsync_node_translation_attached_node(&$node, &$translation, $field) { if (isset($node->$field) && $attached = node_load($node->$field)) { if (translation_supported_type($attached->type)) { // This content type has translations, find the one if (($attachedtrans = translation_node_get_translations($attached)) && isset($attachedtrans[$translation->language])) { $translation->$field = $attachedtrans[$translation->language]->nid; } } else { // Content type without language, just copy the nid $translation->$field = $node->$field; } } } /** * Returns list of fields to synchronize for a given content type * * @param $type * Node type */ function i18nsync_node_fields($type) { return variable_get('i18nsync_nodeapi_'.$type, array()); } /** * Returns list of available fields for given content type * * @param $type * Node type * @param $tree * Whether to return in tree form or FALSE for flat list */ function i18nsync_node_available_fields($type) { // Default node fields $fields['node']['#title'] = t('Standard node fields.'); $options = variable_get('i18nsync_fields_node', array()); $options += array( 'author' => t('Author'), 'status' => t('Status'), 'promote' => t('Promote'), 'moderate' => t('Moderate'), 'sticky' => t('Sticky'), 'revision' => t('Revision (Create also new revision for translations)'), 'parent' => t('Book outline (With the translated parent)'), 'taxonomy' => t('Taxonomy terms'), ); if (module_exists('comment')) { $options['comment'] = t('Comment settings'); } if (module_exists('upload') || module_exists('image')) { $options['files'] = t('File attachments'); } // If no type defined yet, that's it $fields['node']['#options'] = $options; if (!$type) { return $fields; } // Get variable for this node type $fields += variable_get("i18nsync_fields_node_$type", array()); // Image attach if (variable_get('image_attach_'. $type, 0)) { $fields['image']['#title'] = t('Image Attach module'); $fields['image']['#options']['iid'] = t('Attached image nodes'); } // Event fields if (variable_get('event_nodeapi_'. $type, 'never') != 'never') { $fields['event']['#title'] = t('Event fields'); $fields['event']['#options'] = array( 'event_start' => t('Event start'), 'event_end' => t('Event end'), 'timezone' => t('Timezone') ); } // Get CCK fields /* Disabled until there's a stable version of CCK + views if (($content = module_invoke('content', 'types', $type)) && isset($content['fields'])) { // Get context information $info = module_invoke('content', 'fields', NULL, $type); $fields['cck']['#title'] = t('CCK fields'); foreach ($content['fields'] as $name => $data) { $fields['cck']['#options'][$data['field_name']] = $data['widget']['label']; } } */ return $fields; } /* * Sample CCK field definition for Drupal 5 'field_text' => array 'field_name' => string 'field_text' (length=10) 'type' => string 'text' (length=4) 'required' => string '0' (length=1) 'multiple' => string '1' (length=1) 'db_storage' => string '0' (length=1) 'text_processing' => string '0' (length=1) 'max_length' => string '' (length=0) 'allowed_values' => string '' (length=0) 'allowed_values_php' => string '' (length=0) 'widget' => array ... 'type_name' => string 'test' (length=4) */