'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)
*/