cvs diff: Diffing . ? .project Index: skeleton.install =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/skeleton/skeleton.install,v retrieving revision 1.6 diff -u -p -r1.6 skeleton.install --- skeleton.install 12 Mar 2009 18:22:29 -0000 1.6 +++ skeleton.install 12 Jan 2010 17:36:02 -0000 @@ -72,6 +72,26 @@ function skeleton_schema() { 'template' => array('type' => 'varchar', 'length' => 80), 'node_type' => array('type' => 'varchar', 'length' => 80), 'node_data' => array('type' => 'blob', 'size' => 'big'), + 'language' => array( + 'description' => 'The {languages}.language of this node.', + 'type' => 'varchar', + 'length' => 12, + 'not null' => TRUE, + 'default' => '' + ), + 'ttid' => array( + 'description' => 'The translation set id for this template, which equals the template id of the source post in each set.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0 + ), + 'translate' => array( + 'description' => 'A boolean indicating whether this translation template needs to be updated.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0 + ), ), 'primary key' => array( 'template_id', @@ -149,3 +169,82 @@ function skeleton_update_6002() { db_create_table($ret, 'skeleton_template_node', $schema['skeleton_template_node']); return $ret; } + +/** + * Add a table for storing template translations. + */ +function skeleton_update_6003() { + $ret = array(); + $schema = array(); + $schema['skeleton_template'] = array( + 'fields' => array( + 'language' => array( + 'description' => 'The {languages}.language of this node.', + 'type' => 'varchar', + 'length' => 12, + 'not null' => TRUE, + 'default' => '' + ), + 'ttid' => array( + 'description' => 'The translation set id for this template, which equals the template id of the source post in each set.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0 + ), + 'translate' => array( + 'description' => 'A boolean indicating whether this translation template needs to be updated.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0 + ), + ), + ); + db_add_field($ret, 'skeleton_template', 'language', $schema['skeleton_template']['fields']['language']); + db_add_field($ret, 'skeleton_template', 'ttid', $schema['skeleton_template']['fields']['ttid']); + db_add_field($ret, 'skeleton_template', 'translate', $schema['skeleton_template']['fields']['translate']); + db_add_index($ret, 'skeleton_template', 'ttid', array('ttid')); + db_add_index($ret, 'skeleton_template', 'translate', array('translate')); + + // We store the language for each template in it's own column so we don't + // have search the serialized node to filter on language. + $batch = array( + 'title' => t('Saving skeleton template languages'), + 'file' => drupal_get_path('module', 'skeleton') .'/skeleton.install', + ); + $batch['operations'][] = array('_skeleton_template_extract_language', array()); + $ret[] = array( + 'query' => t('Saved the language of each template to its own column.'), + 'success' => TRUE, + ); + batch_set($batch); + + return $ret; +} + +/** + * Batch API callback to extract the language from each node into the language + * column in the {skeleton_template} table. + */ +function _skeleton_template_extract_language(&$context) { + $t = get_t(); + if (empty($context['sandbox'])) { + $context['sandbox']['template_ids'] = array(); + $result = db_query("SELECT template_id FROM {skeleton_template}"); + while ($template_id = db_result($result)) { + $context['sandbox']['template_ids'][] = $template_id; + } + $context['sandbox']['max'] = count($context['sandbox']['template_ids']); + $context['sandbox']['progress'] = 0; + } + $current_template = skeleton_template_load(array_shift($context['sandbox']['template_ids'])); + $context['message'] = $current_template->template; + if (isset($current_template->node_data['language'])) { + db_query("UPDATE {skeleton_template} SET language = '%s' WHERE template_id = %d", $current_template->node_data['language'], $current_template->template_id); + $context['results'][] = $t("Set language for !template to !language.", array('!template' => $current_template->template, '!language' => $current_template->node_data['language'])); + } + $context['sandbox']['progress']++; + if ($context['sandbox']['progress'] != $context['sandbox']['max']) { + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; + } +} Index: skeleton.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/skeleton/skeleton.module,v retrieving revision 1.20 diff -u -p -r1.20 skeleton.module --- skeleton.module 27 Aug 2009 19:17:08 -0000 1.20 +++ skeleton.module 12 Jan 2010 17:36:02 -0000 @@ -207,12 +207,25 @@ function skeleton_menu() { 'weight' => 10, 'file' => 'skeleton_instance.inc', ); + + // AHAH callbacks. $items['skeleton/introduction'] = array( 'page callback' => 'skeleton_introduction_js', 'access arguments' => array('create new instances'), 'type' => MENU_CALLBACK, 'file' => 'skeleton_instance.inc', ); + + // Autocomplete callbacks. + if (module_exists('translate')) { + $items['skeleton/template/autocomplete'] = array( + 'title' => 'Template title autocomplete', + 'page callback' => 'skeleton_template_autocomplete', + 'access arguments' => array('configure skeleton outlines'), + 'type' => MENU_CALLBACK, + 'file' => 'skeleton_translate.inc', + ); + } $result = db_query("SELECT skeleton_id, skeleton FROM {skeleton} ORDER BY skeleton_id"); while ($skeleton = db_fetch_object($result)) { @@ -279,6 +292,10 @@ function skeleton_theme() { 'skeleton_na_form_element' => array( 'arguments' => array('form' => NULL), ), + 'skeleton_template_select_translation' => array( + 'arguments' => array('element' => NULL), + 'file' => 'skeleton_translate.inc', + ), ); } Index: skeleton_sync.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/skeleton/skeleton_sync.inc,v retrieving revision 1.3 diff -u -p -r1.3 skeleton_sync.inc --- skeleton_sync.inc 27 Aug 2009 17:36:57 -0000 1.3 +++ skeleton_sync.inc 12 Jan 2010 17:36:02 -0000 @@ -13,6 +13,11 @@ function skeleton_sync_page() { $output .= drupal_get_form('skeleton_sync_content_form'); $output .= drupal_get_form('skeleton_sync_add_template_form'); $output .= drupal_get_form('skeleton_sync_delete_template_form'); + // TOOD: When skeleton syncing becomes proper tabs, move this to hook_menu(). + if (module_exists('translation')) { + module_load_include('inc', 'skeleton', 'skeleton_translate'); + $output .= drupal_get_form('skeleton_sync_translation_form'); + } return $output; } Index: skeleton_template.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/skeleton/skeleton_template.inc,v retrieving revision 1.20 diff -u -p -r1.20 skeleton_template.inc --- skeleton_template.inc 27 Aug 2009 18:51:01 -0000 1.20 +++ skeleton_template.inc 12 Jan 2010 17:36:02 -0000 @@ -267,6 +267,13 @@ function skeleton_alter_node_form(&$form '#description' => t('The node type for this template. This value cannot be edited.'), '#disabled' => TRUE ); + + // Enable translatable template support. + if (module_exists('translation') && translation_supported_type($template->node_type)) { + module_load_include('inc', 'skeleton', 'skeleton_translate'); + _skeleton_translate_template_form($form, $form_state, $form_id, $template, $node); + } + $form['skeleton_template']['template_id'] = array('#type' => 'value', '#value' => $template->template_id); // Add token help information below the skeleton fieldset. @@ -396,13 +403,21 @@ function theme_skeleton_na_form_element( * FormsAPI */ function skeleton_edit_template_form_validate($form, &$form_state) { - // stub function + if (module_exists('translation') && isset($form['skeleton_template']['translations'])) { + module_load_include('inc', 'skeleton', 'skeleton_translate'); + _skeleton_translate_template_form_validate($form, $form_state); + } } /** * FormsAPI for handling the template form */ function skeleton_edit_template_form_submit($form, &$form_state) { + if (module_exists('translation') && isset($form['skeleton_template']['translations'])) { + module_load_include('inc', 'skeleton', 'skeleton_translate'); + _skeleton_translate_template_form_submit($form, $form_state); + } + // these items are not stored node data $node = $form_state['values']; $unset = array('template', 'node_type', 'nid', 'vid', 'teaser_js', 'created', 'changed', 'date', 'op', 'submit', 'form_token', 'form_id', '#after_build'); Index: skeleton_translate.inc =================================================================== RCS file: skeleton_translate.inc diff -N skeleton_translate.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ skeleton_translate.inc 12 Jan 2010 17:36:02 -0000 @@ -0,0 +1,418 @@ +ttid == 0) { + return array(); + } + $result = db_query("SELECT template_id FROM {skeleton_template} WHERE ttid = %d", $template->ttid); + $translations = array($template->language => $template); + while ($translation_id = db_result($result)) { + // We could do a SELECT * and reduce the number of DB queries, but that + // would require replicating the code in the loader function, which could + // be a pain down the line. + $translation = skeleton_template_load($translation_id); + $translations[$translation->language] = $translation; + } + return $translations; +} + +/** + * Display a list of all books to propagate translation mappings to. Each + * submitted set of checkboxes should correspond to a set of translation sets, + * with each node in a set belonging to a different book. + */ +function skeleton_sync_translation_form($form_state) { + $form = array(); + $books = book_get_books(); + $book_options = array(); + foreach($books as $bid => $book) { + $book_options[$bid] = $book['title']; + } + $form['translation'] = array( + '#type' => 'fieldset', + '#title' => t('Syncronize node translations'), + ); + $form['translation']['books'] = array( + '#type' => 'checkboxes', + '#title' => t('Syncronize node translations within these books'), + '#options' => $book_options, + ); + $form['translation']['submit'] = array( + '#type' => 'submit', + '#value' => t('Update translations'), + ); + return $form; +} + +/** + * FormsAPI validation hook for the translation sync form. The user must select + * at least two books, and every selected book must be of a different language. + * Drupal prevents nodes of different languages from being in the same book, + * greatly simplifying the validation. + */ +function skeleton_sync_translation_form_validate($form, &$form_state) { + $selected_books = array_filter($form_state['values']['books']); + if (count($selected_books) < 2) { + form_set_error('books', t('Select at least 2 books in different languages.')); + } + $languages = array(); + foreach ($selected_books as $book) { + $book = node_load($book); + if (!in_array($book->language['language'], $languages)) { + $languages[] = $book->language['language']; + } + else { + form_set_error('books', t('Multiple books of the same language were selected.')); + } + } +} + +/** + * FormsAPI submit handler for the translation propagation form. On submit, + * the form finds every node within each book which was instantiated from a + * skeleton template. These templates are then built into their associated + * template translation sets, which are used to calculate a translation source + * for each node. + * + * TODO: This is probably a good candidate for a Batch API function. + */ +function skeleton_sync_translation_form_submit($form, &$form_state) { + $translations = array(); + $selected_books = array_filter($form_state['values']['books']); + foreach ($selected_books as $bid) { + $result = db_query("SELECT b.nid, stn.template_id, st.ttid FROM {book} b INNER JOIN {skeleton_template_node} stn ON b.nid = stn.nid INNER JOIN {skeleton_template} st ON stn.template_id = st.template_id WHERE bid = %d", $bid); + while ($row = db_fetch_object($result)) { + // Exclude templates not in a translation set. + if ($row->ttid > 0) { + $translations[$row->ttid][$row->template_id] = node_load($row->nid); + } + } + } + foreach ($translations as $ttid => $set) { + // Sanity check to ensure that the set has more then one template + // associated. + if (count($set) > 1) { + $tnid = $set[$ttid]->nid; + foreach ($set as $ttid => $node) { + db_query("UPDATE {node} SET tnid = %d WHERE nid = %d AND tnid <> 0", $tnid, $node->nid); + } + } + } + drupal_set_message(t("Template translations have been saved.")); +} + +/** + * Generates 'title [nid:$nid]' for the autocomplete field. + */ +function skeleton_template_tid2autocomplete($template) { + return check_plain($template->template) . ' [tid:' . $template->template_id .']'; +} + +/** + * Reverse mapping from template title to tid. + * + * We also handle autocomplete values (title [tid:x]) and validate the form + */ +function skeleton_template_autocomplete2nid($name, $field = NULL, $type, $language) { + if (!empty($name)) { + preg_match('/^(?:\s*|(.*) )?\[\s*tid\s*:\s*(\d+)\s*\]$/', $name, $matches); + if (!empty($matches)) { + // Explicit [tid:n]. + list(, $title, $ttid) = $matches; + if (!empty($title) && ($template = skeleton_template_load($ttid)) && $title != $template->template) { + if ($field) { + form_set_error($field, t('Template title mismatch. Please check your selection.')); + } + $ttid = NULL; + } + } + else { + // No explicit tid. + $reference = _skeleton_template_references($name, 'equals', array('node_type' => $type, 'language' => $language), 1); + if (!empty($reference)) { + $ttid = key($reference); + } + elseif ($field) { + form_set_error($field, t('Found no valid template with that title: %title', array('%title' => $name))); + } + } + } + return !empty($ttid) ? $ttid : NULL; +} + +/** + * FormsAPI alter function to enable template translations. Much of this is + * from the i18n module. + * + * @param $form + * The form array from hook_form_alter(). + * + * @param $form_state + * The form state from hook_form_alter(). + * + * @param $form_id + * The form id from hook_form_alter(). + * + * @param $template + * The current skeleton template being edited. + * + * @param $node + * The representation of the node data for the current template as an array. + */ +function _skeleton_translate_template_form(&$form, $form_state, $form_id, $template, $node) { + // The translation set ID is either set, so load the source translation, or + // this template will be the new source translation. + $template->ttid != 0 ? $source_translation = skeleton_template_load($template->ttid) : $source_translation = $template; + + $form['skeleton_template']['source_translation'] = array( + '#type' => 'hidden', + '#value' => $source_translation->ttid + ); + + $translations = skeleton_template_load_translations($source_translation); + + // If there is a translation for this template, ensure that the user doesn't + // select a translated language for this template. + if (isset($form['language']['#options'])) { + foreach (array_keys($translations) as $language) { + if ($language != $template->node_data['language']) { + unset($form['language']['#options'][$language]); + } + } + } + + $form['skeleton_template']['translations'] = array( + '#type' => 'fieldset', + '#title' => t('Select translations for %title', array('%title' => $source_translation->template)), + '#tree' => TRUE, + '#theme' => 'skeleton_template_select_translation', + '#description' => t("You can select existing templates as translations of this one or remove templates from this translation set. Only templates that have the right language and don't belong to another translation set will be available here.") + ); + + // Build the language autocomplete dropdowns. + foreach (language_list() as $language) { + if ($language->language != $template->node_data['language']) { + $trans_tid = isset($translations[$language->language]) ? $translations[$language->language]->template_id : 0; + $form['skeleton_template']['translations']['ttid'][$language->language] = array( + '#type' => 'value', + '#value' => $trans_tid + ); + + // Special formating for the source language label. + if (!empty($trans_tid) && $trans_tid == $template->ttid) { + $form['skeleton_template']['translations']['language'][$language->language] = array( + '#value' => t('@language_name (source)', array('@language_name' => $language->name)) + ); + } + else { + $form['skeleton_template']['translations']['language'][$language->language] = array( + '#value' => $language->name + ); + } + + $form['skeleton_template']['translations']['template'][$language->language] = array( + '#type' => 'textfield', + '#autocomplete_path' => 'skeleton/template/autocomplete/' . $source_translation->node_type . '/' . $language->language, + '#default_value' => $trans_tid ? skeleton_template_tid2autocomplete($translations[$language->language]) : '', + ); + } + } + + // The "retranslate all translations" checkbox, or the "needs updating" flag. + if ($template->template_id == $template->ttid) { + $form['skeleton_template']['translations']['translate_retranslate'] = array( + '#type' => 'checkbox', + '#title' => t('Flag translations as outdated'), + '#description' => t('If you made a significant change, which means translations should be updated, you can flag all translations of this post as outdated. This will not change any other property of those posts, like whether they are published or not.'), + ); + } + else { + $form['skeleton_template']['translations']['translate_status'] = array( + '#type' => 'checkbox', + '#title' => t('This translation needs to be updated'), + '#description' => t('When this option is checked, this translation needs to be updated because the source post has changed. Uncheck when the translation is up to date again.'), + '#default_value' => $template->translate, + ); + } +} + +/** + * Validation handler for the translation portion of the template form. + */ +function _skeleton_translate_template_form_validate($form, &$form_state) { + foreach ($form_state['values']['translations']['template'] as $lang => $title) { + if (!$title) { + $ttid = 0; + } + else { + $ttid = skeleton_template_autocomplete2nid($title, "translations][template][$lang", array($template->node_type), array($lang)); + } + $form_state['values']['translations']['ttid'][$lang] = $ttid; + } +} + +/** + * Submit handler for the translation portion of the template form. + */ +function _skeleton_translate_template_form_submit($form, &$form_state) { + $node = $form_state['values']; + + // Save the language of the node for easy access. + if (isset($node['language'])) { + db_query("UPDATE {skeleton_template} SET language = '%s' WHERE template_id = %d", $node['language'], $form_state['values']['template_id']); + } + + // Get all of the current translations in the database. + $translations = skeleton_template_load_translations(skeleton_template_load($form_state['values']['template_id'])); + $current_translations = array(); + foreach ($translations as $trans) { + $current_translations[$trans->language] = $trans->template_id; + } + $update = array($node['language'] => $form_state['values']['template_id']) + array_filter($form_state['values']['translations']['ttid']); + + // Compute the difference to see which are the new translations and which ones to remove. + $new = array_diff_assoc($update, $current_translations); + $remove = array_diff_assoc($current_translations, $update); + + // The tricky part: If the existing source is not in the new set, we need to create a new ttid. + if (array_search($form_state['values']['ttid'], $update)) { + $ttid = $form_state['values']['source_translation']; + $add = $new; + } + else { + // Create new ttid, which is the source template. + $ttid = $form_state['values']['template_id']; + $add = $update; + } + + // Now update values for all templates. + if ($add) { + $args = array('' => $ttid) + $add; + db_query('UPDATE {skeleton_template} SET ttid = %d WHERE template_id IN (' . db_placeholders($add) . ')', $args); + if (count($new)) { + drupal_set_message(format_plural(count($new), 'Added a template to the translation set.', 'Added @count templates to the translation set.')); + } + } + + if ($remove) { + if (count($update) == 1) { + $remove += array($node['language'] => $form_state['values']['template_id']); + } + db_query('UPDATE {skeleton_template} SET ttid = 0 WHERE template_id IN (' . db_placeholders($remove) . ')', $remove); + drupal_set_message(format_plural(count($remove), 'Removed a template from the translation set.', 'Removed @count templates from the translation set.')); + } + + // Flag all translations as needing to be updated. + if (!empty($node['translations']['translate_retranslate'])) { + foreach ($translations as $trans) { + if ($trans->template_id != $trans->ttid) { + db_query("UPDATE {skeleton_template} SET translate = 1 WHERE template_id = %d", $trans->template_id); + } + } + } + + // Flag for this translation only. + if (!empty($node['translations']['translate_status'])) { + db_query("UPDATE {skeleton_template} SET translate = 1 WHERE template_id = %d", $form_state['values']['template_id']); + } + else { + db_query("UPDATE {skeleton_template} SET translate = 0 WHERE template_id = %d", $form_state['values']['template_id']); + } + + unset($form_state['values']['translations']); +} + +/** + * Theme select translation form + * @ingroup themeable + */ +function theme_skeleton_template_select_translation(&$elements) { + $output = ''; + if (isset($elements['ttid'])) { + $rows = array(); + foreach (element_children($elements['ttid']) as $lang) { + $rows[] = array( + drupal_render($elements['language'][$lang]), + drupal_render($elements['template'][$lang]), + ); + } + $output .= theme('table', array(), $rows); + $output .= drupal_render($elements); + } + return $output; +} + +/** + * Template title autocomplete callback. + */ +function skeleton_template_autocomplete($type, $language, $string = '') { + $params = array('node_type' => $type, 'ttid' => 0, 'language' => $language); + $matches = array(); + foreach (_skeleton_template_references($string, 'contains', $params) as $id => $row) { + // Add a class wrapper for a few required CSS overrides. + $matches[$row['title'] ." [tid:$id]"] = '