You are here

i18nsync.module in Internationalization 5.2

Same filename and directory in other branches
  1. 5.3 experimental/i18nsync.module
  2. 5 experimental/i18nsync.module

Internationalization (i18n) package. Synchronization of translations

Keeps vocabulary terms in sync for translations. This is a per-vocabulary option

Ref: http://drupal.org/node/115463

Notes: This module needs to run after taxonomy, i18n, translation. Check module weight

File

experimental/i18nsync.module
View source
<?php

/**
 * @file
 * Internationalization (i18n) package. Synchronization of translations
 * 
 * Keeps vocabulary terms in sync for translations.
 * This is a per-vocabulary option
 * 
 * Ref: http://drupal.org/node/115463
 * 
 * Notes:
 * This module needs to run after taxonomy, i18n, translation. Check module weight
 */

/**
 * Implementation of hook_form_alter().
 */
function i18nsync_form_alter($form_id, &$form) {

  // Taxonomy vocabulary form
  switch ($form_id) {
    case 'taxonomy_form_vocabulary':
      $nodesync = variable_get('i18n_vocabulary_nodesync', array());
      $form['i18n']['nodesync'] = array(
        '#type' => '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'] ? '<div class="description">' . $element['#group_title'] . '</div>' : '';
  $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') ", $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 " . " WHERE tn.nid = %d AND n.nid != %d AND n.trid = %d AND td.vid = %d " . " AND td.language = '' OR td.language IS NULL", $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 " . " INNER JOIN {term_data} td ON tdt.trid = td.trid " . " INNER JOIN {term_node} tn ON tn.tid = td.tid " . " WHERE tdt.trid AND tdt.language = n.language " . " 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)
*/

Functions

Namesort descending Description
i18nsync_form_alter Implementation of hook_form_alter().
i18nsync_nodeapi Implementation of hook_nodeapi().
i18nsync_node_available_fields Returns list of available fields for given content type
i18nsync_node_fields Returns list of fields to synchronize for a given content type
i18nsync_node_taxonomy Actual synchronization for node, vocabulary
i18nsync_node_translation
i18nsync_node_translation_attached_node Node attachments that may have translation
i18nsync_taxonomy Implementation of hook taxonomy.
theme_i18nsync_workflow_checkbox