Updated translation synchronization
[project/i18n.git] / i18nsync / i18nsync.module
... / ...
CommitLineData
1<?php
2// $Id$
3
4/**
5 * @file
6 * Internationalization (i18n) package. Synchronization of translations
7 *
8 * Keeps vocabulary terms in sync for translations.
9 * This is a per-vocabulary option
10 *
11 * Ref: http://drupal.org/node/115463
12 *
13 * Notes:
14 * This module needs to run after taxonomy, i18n, translation. Check module weight
15 *
16 * @ TODO Test with CCK when possible, api may have changed
17 */
18
19/**
20 * Implementation of hook_help().
21 */
22function i18nsync_help($section, $arg) {
23 switch ($section) {
24 case 'admin/help#i18nsync' :
25 $output = '<p>'.t('This module synchronizes content taxonomy and field accross translations:').'</p>';
26 $output .= '<p>'.t('First you need to select which vocabularies and fields should be synchronized. Then, after a node has been updated, all enabled vocabularies and fields will be synchronized as follows:').'</p>';
27 $output .= '<ul>';
28 $output .= '<li>'.t('All the node fields selected for synchronization will be set to the same value for all translations').'</li>';
29 $output .= '<li>'.t('For multilingual vocabularies, the terms for all translations will be replaced by the translations of the original node terms.').'</li>';
30 $output .= '<li>'.t('For other vocabularies, the terms will be just copied over to all the translations.');
31 $output .= '</ul>';
32 $output .= '<p><strong>'.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.').'</strong></p>';
33 $output .= '<p>'.t('To enable synchronization:').'</p>';
34 $output .= '<ul>';
35 $output .= '<li>'.t('Check vocabulary options to enable synchronization for each vocabulary').'</li>';
36 $output .= '<li>'.t('Check content type options to select which fields to synchronize for each content type').'</li>';
37 $output .= '</ul>';
38 $output .= '<p>'.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.').'</p>';
39 $output .= '<p>'. t('For more information please read the <a href="@i18n">on-line help pages</a>.', array('@i18n' =>'http://drupal.org/node/31631')) .'</p>';
40 return $output;
41 }
42}
43
44/**
45 * Implementation of hook_theme()
46 */
47function i18nsync_theme() {
48 return array(
49 'i18nsync_workflow_checkbox' => array(
50 'arguments' => array('item' => NULL),
51 ),
52 );
53}
54
55/**
56 * Implementation of hook_form_alter().
57 * - Vocabulary options
58 * - Content type options
59 */
60function i18nsync_form_alter(&$form, $form_state, $form_id) {
61 // Taxonomy vocabulary form
62 switch ($form_id) {
63 case 'taxonomy_form_vocabulary':
64 $vid = isset($form['vid']['#value']) ? $form['vid']['#value'] : NULL;
65 $nodesync = variable_get('i18n_vocabulary_nodesync', array());
66 $form['i18n']['nodesync'] = array(
67 '#type' => 'checkbox', '#title' => t('Synchronize node translations'),
68 '#default_value' => $vid && isset($nodesync[$vid]) ? $nodesync[$vid] : 0,
69 '#description' => t('Synchronize terms of this vocabulary for node translations.')
70 );
71 break;
72 case 'node_type_form':
73 $type = $form['#node_type']->type;
74 $current = i18nsync_node_fields($type);
75
76 $form['workflow']['i18n']['i18nsync_nodeapi'] = array(
77 '#type' => 'fieldset', '#tree' => TRUE,
78 '#title' => t('Synchronize translations'),
79 '#collapsible' => TRUE,
80 '#collapsed' => !count($current),
81 '#description' => t('Select which fields to synchronize for all translations of this content type.')
82 );
83 // Each set provides title and options. We build a big checkboxes control for it to be
84 // saved as an array. Special themeing for group titles.
85 foreach (i18nsync_node_available_fields($type) as $group => $data) {
86 $title = $data['#title'];
87 foreach ($data['#options'] as $field => $name) {
88 $form['workflow']['i18n']['i18nsync_nodeapi'][$field] = array(
89 '#group_title' => $title,
90 '#title' => $name,
91 '#type' => 'checkbox',
92 '#default_value' => in_array($field, $current),
93 '#theme' => 'i18nsync_workflow_checkbox',
94 );
95 $title = '';
96 }
97 }
98
99 break;
100 }
101}
102
103/**
104 * Theming function for workflow checkboxes
105 */
106function theme_i18nsync_workflow_checkbox($element){
107 $output = $element['#group_title'] ? '<div class="description">'.$element['#group_title'].'</div>' : '';
108 $output .= theme('checkbox', $element);
109 return $output;
110}
111
112/**
113 * Implementation of hook taxonomy.
114 */
115function i18nsync_taxonomy($op, $type = NULL, $edit = NULL) {
116 switch ("$type/$op") {
117 case 'vocabulary/insert':
118 case 'vocabulary/update':
119 $current = variable_get('i18n_vocabulary_nodesync', array());
120 if ($edit['nodesync']) {
121 $current[$edit['vid']] = 1;
122 } else {
123 unset($current[$edit['vid']]);
124 }
125 variable_set('i18n_vocabulary_nodesync', $current);
126 break;
127 }
128}
129
130/**
131 * Implementation of hook_nodeapi().
132 *
133 * Note that we avoid getting node parameter by reference
134 */
135function i18nsync_nodeapi($node, $op, $a3 = NULL, $a4 = NULL) {
136 global $i18nsync; // This variable will be true when a sync operation is in progress
137
138 // Only for nodes that have language and belong to a translation set.
139 if (variable_get("i18n_node_$node->type", 0) && $node->language && !empty($node->tnid) && !$i18nsync) {
140 switch ($op) {
141 case 'insert':
142 case 'update':
143 // Taxonomy synchronization
144 if ($sync = variable_get('i18n_vocabulary_nodesync', array())) {
145
146 // Get vocabularies synchronized for this node type
147 $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)));
148 $count = 0;
149 while ($vocabulary = db_fetch_object($result)) {
150 i18nsync_node_taxonomy($node, $vocabulary);
151 $count++;
152 }
153 if ($count) {
154 drupal_set_message(t('Node taxonomy has been synchronized.'));
155 }
156 } // No need to refresh cache. It will be refreshed after insert/update anyway
157
158 // Let's go with field synchronization.
159 if (($fields = i18nsync_node_fields($node->type)) && ($translations = translation_node_get_translations($node->tnid)) && count($translations) > 1) {
160 // We want to work with a fresh copy of this node, so we load it again bypassing cache.
161 // This is to make sure all the other modules have done their stuff and the fields are right.
162 // But we have to copy the revision field over to the new copy.
163 $revision = isset($node->revision) && $node->revision;
164 $node = node_load(array('nid' => $node->nid));
165 $node->revision = $revision;
166 $i18nsync = TRUE;
167
168 foreach ($translations as $trnode) {
169 if ($node->nid != $trnode->nid) {
170 i18nsync_node_translation($node, $trnode, $fields);
171 }
172 }
173 $i18nsync = FALSE;
174 drupal_set_message(t('All %count node translations have been synchronized', array('%count' => count($translations) - 1)));
175 }
176 break;
177 }
178 }
179}
180
181/**
182 * Synchronizes fields for node translation
183 *
184 * There's some specific handling for known fields like:
185 * - files, for file attachments
186 * - iid (CCK node attachments, translations for them will be handled too)
187 *
188 * All the rest of the fields will be just copied over.
189 * The 'revision' field will have the special effect of creating a revision too for the translation
190 *
191 * @param $node
192 * Source node being edited
193 * @param $translation
194 * Node translation to synchronize, just needs nid property
195 * @param $fields
196 * List of fields to synchronize
197 */
198function i18nsync_node_translation($node, $translation, $fields) {
199 // Load full node, we need all data here
200 $translation = node_load($translation->nid);
201 foreach ($fields as $field) {
202 switch($field) {
203 case 'parent': // Book outlines, translating parent page if exists
204 case 'iid': // Attached image nodes
205 i18nsync_node_translation_attached_node(&$node, &$translation, $field);
206 break;
207 case 'files':
208 // Sync existing attached files
209 foreach ($node->files as $fid => $file) {
210 if (isset($translation->files[$fid])) {
211 $translation->files[$fid]->list = $file->list;
212 } else {
213 // New file. Create new revision of file for the translation
214 $translation->files[$fid] = $file;
215 // If it's a new node revision it will just be created, but if it's not
216 // we have to update the table directly. The revision field was before this one in the list
217 if (!isset($translation->revision) || !$translation->revision) {
218 db_query("INSERT INTO {file_revisions} (fid, vid, list, description) VALUES (%d, %d, %d, '%s')", $file->fid, $translation->vid, $file->list, $file->description);
219 }
220 }
221 }
222 // Drop removed files
223 foreach($translation->files as $fid => $file) {
224 if (!isset($node->files[$fid])) {
225 $translation->files[$fid]->remove = TRUE;
226 }
227 }
228 break;
229 default: // For fields that don't need special handling
230 if (isset($node->$field)) {
231 $translation->$field = $node->$field;
232 }
233 }
234 }
235 node_save($translation);
236}
237
238/**
239 * Node attachments (CCK) that may have translation
240 */
241function i18nsync_node_translation_attached_node(&$node, &$translation, $field) {
242 if (isset($node->$field) && $attached = node_load($node->$field)) {
243 if (translation_supported_type($attached->type)) {
244 // This content type has translations, find the one
245 if (($attachedtrans = translation_node_get_translations($attached)) && isset($attachedtrans[$translation->language])) {
246 $translation->$field = $attachedtrans[$translation->language]->nid;
247 }
248 } else {
249 // Content type without language, just copy the nid
250 $translation->$field = $node->$field;
251 }
252 }
253}
254
255/**
256 * Synchronization for node taxonomy
257 *
258 * These are the 'magic' db queries.
259 */
260function i18nsync_node_taxonomy($node, $vocabulary) {
261 // Paranoid extra check. This queries may really delete data
262 if ($vocabulary->language || !$node->nid || !$node->tnid || !$node->language || !$vocabulary->vid) return;
263
264 // Reset all terms for this vocabulary for other nodes in the translation set
265 // First delete all terms without language
266 db_query("DELETE FROM {term_node} WHERE nid != %d ".
267 " AND nid IN (SELECT nid FROM {node} WHERE tnid = %d) ".
268 " AND tid IN (SELECT tid FROM {term_data} WHERE (language = '' OR language IS NULL) AND vid = %d) ",
269 $node->nid, $node->tnid, $vocabulary->vid);
270
271 // Now delete all terms which have a translation in the node language
272 // We don't touch the terms that have language but no translation
273 db_query("DELETE FROM {term_node} WHERE nid != %d ".
274 " AND nid IN (SELECT nid FROM {node} WHERE tnid = %d) ".
275 " AND tid IN (SELECT td.tid FROM {term_data} td INNER JOIN {term_data} tt ON td.trid = tt.trid ".
276 " WHERE td.vid = %d AND td.trid AND tt.language = '%s') ", // These are all the terms with translation
277 $node->nid, $node->tnid, $vocabulary->vid, $node->language);
278
279 // Copy terms with no language
280 db_query("INSERT INTO {term_node}(tid, nid) SELECT tn.tid, n.nid " .
281 " FROM {node} n , {term_node} tn " .
282 " INNER JOIN {term_data} td ON tn.tid = td.tid " . // This one to check no language
283 " WHERE tn.nid = %d AND n.nid != %d AND n.tnid = %d AND td.vid = %d " .
284 " AND td.language = '' OR td.language IS NULL", // Only terms without language
285 $node->nid, $node->nid, $node->tnid, $vocabulary->vid);
286
287 // Now copy terms translating on the fly
288 db_query("INSERT INTO {term_node}(tid, nid) SELECT tdt.tid, n.nid " .
289 " FROM {node} n , {term_data} tdt " . // This will be term data translations
290 " INNER JOIN {term_data} td ON tdt.trid = td.trid " . // Same translation set
291 " INNER JOIN {term_node} tn ON tn.tid = td.tid " .
292 " WHERE tdt.trid AND tdt.language = n.language " . // trid cannot be 0 or NULL
293 " AND n.nid != %d AND tn.nid = %d AND n.tnid = %d AND td.vid = %d",
294 $node->nid, $node->nid, $node->tnid, $vocabulary->vid);
295}
296
297/**
298 * Returns list of fields to synchronize for a given content type
299 *
300 * @param $type
301 * Node type
302 */
303function i18nsync_node_fields($type) {
304 return variable_get('i18nsync_nodeapi_'.$type, array());
305}
306/**
307 * Returns list of available fields for given content type
308 *
309 * @param $type
310 * Node type
311 * @param $tree
312 * Whether to return in tree form or FALSE for flat list
313 */
314function i18nsync_node_available_fields($type) {
315 // Default node fields
316 $fields['node']['#title'] = t('Standard node fields.');
317 $options = variable_get('i18nsync_fields_node', array());
318 $options += array(
319 'author' => t('Author'),
320 'status' => t('Status'),
321 'promote' => t('Promote'),
322 'moderate' => t('Moderate'),
323 'sticky' => t('Sticky'),
324 'revision' => t('Revision (Create also new revision for translations)'),
325 'parent' => t('Book outline (With the translated parent)'),
326 );
327 if (module_exists('comment')) {
328 $options['comment'] = t('Comment settings');
329 }
330 if (module_exists('upload') || module_exists('image')) {
331 $options['files'] = t('File attachments');
332 }
333 // If no type defined yet, that's it
334 $fields['node']['#options'] = $options;
335 if (!$type) {
336 return $fields;
337 }
338
339 // Get variable for this node type
340 $fields += variable_get("i18nsync_fields_node_$type", array());
341
342 // Image attach
343 if (variable_get('image_attach_'. $type, 0)) {
344 $fields['image']['#title'] = t('Image Attach module');
345 $fields['image']['#options']['iid'] = t('Attached image nodes');
346 }
347 // Event fields
348 if (variable_get('event_nodeapi_'. $type, 'never') != 'never') {
349 $fields['event']['#title'] = t('Event fields');
350 $fields['event']['#options'] = array(
351 'event_start' => t('Event start'),
352 'event_end' => t('Event end'),
353 'timezone' => t('Timezone')
354 );
355 }
356
357 // Get CCK fields
358 if (($content = module_invoke('content', 'types', $type)) && isset($content['fields'])) {
359 // Get context information
360 $info = module_invoke('content', 'fields', NULL, $type);
361 $fields['cck']['#title'] = t('CCK fields');
362 foreach ($content['fields'] as $name => $data) {
363 $fields['cck']['#options'][$data['field_name']] = $data['widget']['label'];
364 }
365 }
366 return $fields;
367}
368
369/*
370 * Sample CCK field definition
371'field_text' =>
372 array
373 'field_name' => string 'field_text' (length=10)
374 'type' => string 'text' (length=4)
375 'required' => string '0' (length=1)
376 'multiple' => string '1' (length=1)
377 'db_storage' => string '0' (length=1)
378 'text_processing' => string '0' (length=1)
379 'max_length' => string '' (length=0)
380 'allowed_values' => string '' (length=0)
381 'allowed_values_php' => string '' (length=0)
382 'widget' =>
383 array
384 ...
385 'type_name' => string 'test' (length=4)
386*/