'checkbox', '#title' => t('Synchronize node translations'), '#default_value' => isset($form['vid']) && is_numeric($form['vid']['#value']) && $nodesync[$form['vid']['#value']], '#description' => t('Synchronize terms of this vocabulary for node translations.') ); break; case 'node_type_form': $type = (isset($form['old_type']) && isset($form['old_type']['#value'])) ? $form['old_type']['#value'] : NULL; $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) { if (array_key_exists('#options', $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; } } function theme_i18nsync_workflow_checkbox($element){ $output = $element['#group_title'] ? '
'.$element['#group_title'].'
' : ''; $output .= theme('checkbox', $element); return $output; } /** * Implementation of hook taxonomy. */ function i18nsync_taxonomy($op, $type = NULL, $edit = NULL) { switch ("$type/$op") { case 'vocabulary/insert': case 'vocabulary/update': $current = variable_get('i18n_vocabulary_nodesync', array()); if ($edit['nodesync']) { $current[$edit['vid']] = 1; } else { unset($current[$edit['vid']]); } variable_set('i18n_vocabulary_nodesync', $current); break; } } /** * Implementation of hook_nodeapi(). * * Note that we avoid getting node parameter by reference */ 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 (variable_get("i18n_node_$node->type", 0) && $node->language && $node->trid && !$i18nsync) { switch ($op) { case 'insert': case 'update': // Taxonomy synchronization if ($sync = variable_get('i18n_vocabulary_nodesync', array())) { // Get vocabularies synchronized for this node type $result = db_query("SELECT v.* FROM {vocabulary} v INNER JOIN {vocabulary_node_types} n ON v.vid = n.vid WHERE n.type = '%s' AND v.vid IN (%s)", $node->type, implode(',', array_keys($sync))); $count = 0; while($vocabulary = db_fetch_object($result)) { i18nsync_node_taxonomy($node, $vocabulary); $count++; } if ($count) { drupal_set_message(t('Node taxonomy has been synchronized.')); } } // No need to refresh cache. It will be refreshed after insert/update anyway // Let's go with field synchronization if (($fields = i18nsync_node_fields($node->type)) && $translations = translation_node_get_translations(array('nid' => $node->nid), FALSE)) { // We want to work with a fresh copy of this node, so we load it again bypassing cache. // This is to make sure all the other modules have done their stuff and the fields are right. // But we have to copy the revision field over to the new copy. $revision = isset($node->revision) && $node->revision; $node = node_load(array('nid' => $node->nid)); $node->revision = $revision; $i18nsync = TRUE; foreach ($translations as $trnode) { i18nsync_node_translation($node, $trnode, $fields); } $i18nsync = FALSE; drupal_set_message(t('All %count node translations have been synchronized', array('%count' => count($translations)))); } break; } } } function i18nsync_node_translation($node, $translation, $fields) { // Load full node, we need all data here $translation = node_load($translation->nid); foreach ($fields as $field) { switch($field) { 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 {file_revisions} (fid, vid, list, description) VALUES (%d, %d, %d, '%s')", $file->fid, $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); } /** * Node attachments that may have translation * */ function i18nsync_node_translation_attached_node(&$node, &$translation, $field) { if (isset($node->$field) && $attached = node_load($node->$field)) { if (variable_get("i18n_node_$attached->type", 0)) { // This content type has translations, find the one if ($attached->translations && isset($attached->translation[$translation->language])) { $translation->$field = $attached->translation[$translation->language]->nid; } } else { // Content type without language, just copy the nid $translation->$field = $node->$field; } } } /** * Actual synchronization for node, vocabulary * * These are the 'magic' db queries. */ function i18nsync_node_taxonomy($node, $vocabulary) { // Paranoid extra check. This queries may really delete data if ($vocabulary->language || !$node->nid || !$node->trid || !$node->language || !$vocabulary->vid) return; // Reset all terms for this vocabulary for other nodes in the translation set // First delete all terms without language db_query("DELETE FROM {term_node} WHERE nid != %d ". " AND nid IN (SELECT nid FROM {i18n_node} WHERE trid = %d) ". " AND tid IN (SELECT tid FROM {term_data} WHERE (language = '' OR language IS NULL) AND vid = %d) ", $node->nid, $node->trid, $vocabulary->vid); // Now delete all terms which have a translation in the node language // We don't touch the terms that have language but no translation db_query("DELETE FROM {term_node} WHERE nid != %d ". " AND nid IN (SELECT nid FROM {i18n_node} WHERE trid = %d) ". " AND tid IN (SELECT td.tid FROM {term_data} td INNER JOIN {term_data} tt ON td.trid = tt.trid ". " WHERE td.vid = %d AND td.trid AND tt.language = '%s') ", // These are all the terms with translation $node->nid, $node->trid, $vocabulary->vid, $node->language); // Copy terms with no language db_query("INSERT INTO {term_node}(tid, nid) SELECT tn.tid, n.nid " . " FROM {i18n_node} n , {term_node} tn " . " INNER JOIN {term_data} td ON tn.tid = td.tid " . // This one to check no language " WHERE tn.nid = %d AND n.nid != %d AND n.trid = %d AND td.vid = %d " . " AND td.language = '' OR td.language IS NULL", // Only terms without language $node->nid, $node->nid, $node->trid, $vocabulary->vid); // Now copy terms translating on the fly db_query("INSERT INTO {term_node}(tid, nid) SELECT tdt.tid, n.nid " . " FROM {i18n_node} n , {term_data} tdt " . // This will be term data translations " INNER JOIN {term_data} td ON tdt.trid = td.trid " . // Same translation set " INNER JOIN {term_node} tn ON tn.tid = td.tid " . " WHERE tdt.trid AND tdt.language = n.language " . // trid cannot be 0 or NULL " AND n.nid != %d AND tn.nid = %d AND n.trid = %d AND td.vid = %d", $node->nid, $node->nid, $node->trid, $vocabulary->vid); } /** * 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)'), ); 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_$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 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 '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) */