i18nsync.module in Internationalization 5
Same filename and directory in other branches
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.moduleView 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) {
$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
Name | 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 |