i18nsync.module

Tracking 5.x-3.x branch
  1. drupal
    1. 5 contributions/i18n/experimental/i18nsync.module
    2. 6 contributions/i18n/i18nsync/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

Functions & methods

NameDescription
i18nsync_form_alterImplementation of hook_form_alter().
i18nsync_nodeapiImplementation of hook_nodeapi().
i18nsync_node_available_fieldsReturns list of available fields for given content type
i18nsync_node_fieldsReturns list of fields to synchronize for a given content type
i18nsync_node_taxonomyActual synchronization for node, vocabulary
i18nsync_node_translation
i18nsync_node_translation_attached_nodeNode attachments that may have translation
i18nsync_taxonomyImplementation of hook taxonomy.
theme_i18nsync_workflow_checkbox

File

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