diff --git l10n_community/l10n_community.module l10n_community/l10n_community.module index bbd3de7..3d32f95 100644 --- l10n_community/l10n_community.module +++ l10n_community/l10n_community.module @@ -207,33 +207,10 @@ function l10n_community_menu() { $items['translate/details'] = array( 'title' => 'String details', 'page callback' => 'l10n_community_string_details', - 'file' => 'ajax.inc', - 'access arguments' => array('browse translations'), - 'type' => MENU_CALLBACK, - ); - $items['translate/suggestions'] = array( - 'title' => 'String suggestions', - 'page callback' => 'l10n_community_string_suggestions', - 'file' => 'ajax.inc', + 'file' => 'translate.inc', 'access arguments' => array('browse translations'), 'type' => MENU_CALLBACK, ); - $items['translate/approve'] = array( - 'title' => 'Approve suggestion', - 'page callback' => 'l10n_community_string_approve', - 'file' => 'ajax.inc', - // Permission is enforced in l10n_community_string_ajax_suggestion(). - 'access arguments' => array('access localization community'), - 'type' => MENU_CALLBACK, - ); - $items['translate/decline'] = array( - 'title' => 'Decline suggestion', - 'page callback' => 'l10n_community_string_decline', - 'file' => 'ajax.inc', - // Permission is enforced in l10n_community_string_ajax_suggestion(). - 'access arguments' => array('access localization community'), - 'type' => MENU_CALLBACK, - ); // As soon as we have a language code, we can translate. $items['translate/languages/%l10n_community_language'] = array( @@ -251,8 +228,8 @@ function l10n_community_menu() { 'weight' => -20, ); // Tabs to translate, import and export projects. - $items['translate/languages/%l10n_community_language/view'] = array( - 'title' => 'Browse', + $items['translate/languages/%l10n_community_language/translate'] = array( + 'title' => 'Translate', 'page callback' => 'l10n_community_translate_page', 'page arguments' => array(2), 'file' => 'translate.inc', @@ -261,16 +238,6 @@ function l10n_community_menu() { 'type' => MENU_LOCAL_TASK, 'weight' => -10, ); - $items['translate/languages/%l10n_community_language/edit'] = array( - 'title' => 'Translate', - 'page callback' => 'l10n_community_translate_page', - 'page arguments' => array(2, 'edit'), - 'file' => 'translate.inc', - 'access callback' => 'l10n_community_has_permission', - 'access arguments' => array(2, (string)L10N_PERM_SUGGEST), - 'type' => MENU_LOCAL_TASK, - 'weight' => -8, - ); $items['translate/languages/%l10n_community_language/moderate'] = array( 'title' => 'Moderate', 'page callback' => 'l10n_community_moderate_page', @@ -496,6 +463,7 @@ function l10n_community_block_help() { // We are dealing with a groups based permission model. $permission_help[] = l10n_groups_block_help($perm, isset($args['langcode']) ? $args['langcode'] : NULL); } + if ($perm & L10N_PERM_SUGGEST) { $permission_notes[] = t('You can suggest translations to be reviewed by moderators of the team.'); } @@ -949,6 +917,19 @@ function l10n_community_get_languages($key = NULL) { } /** + * Returns a language object for a specific language. + * + * @param $langcode + * Language code, for example 'hu', 'pt-br', 'de' or 'it'. + * @return + * A populated language object. + */ +function l10n_community_get_language($langcode) { + $languages = l10n_community_get_languages(); + return $languages[$langcode]; +} + +/** * Get translation permission level for a specific user. * * @param $langcode @@ -958,7 +939,7 @@ function l10n_community_get_languages($key = NULL) { * @return * A bitmask of all permission flags the user has. */ -function l10n_community_get_permission($langcode, $account = NULL) { +function l10n_community_get_permission($langcode, $account = NULL, $check = NULL) { static $permissions = array(); global $user; @@ -1000,12 +981,47 @@ function l10n_community_get_permission($langcode, $account = NULL) { * @param $langcode * Language code, for example 'hu', 'pt-br', 'de' or 'it'. * @param $permission - * The permission to check for. + * The permission to check for. Note that if you pass multiple permission + * constants, this function will return TRUE if the user has at least one + * of them. * @return * TRUE if the user has at least one of the specified permissions. */ function l10n_community_has_permission($langcode, $permission) { - return (bool)(l10n_community_get_permission($langcode, NULL) & $permission); + return (bool)(l10n_community_get_permission($langcode) & $permission); +} + +/** + * Checks whether the current user may approve a translation. + * + * @param $translation + * The translation in question. + * @return + * TRUE if the user may approve the translation. + */ +function l10n_community_may_approve_translation($translation) { + global $user; + $required = $translation->uid_entered == $user->uid ? L10N_PERM_MODERATE_OWN : L10N_PERM_MODERATE_OTHERS; + return l10n_community_has_permission($translation->language, $required); +} + +/** + * Checks whether the current user may decline a translation. + * + * @param $translation + * The translation in question. + * @return + * TRUE if the user may decline the translation. + */ +function l10n_community_may_decline_translation($translation) { + global $user; + if ($translation->uid_entered == $user->uid) { + // Everyone may decline their own translations. + return TRUE; + } + else { + return l10n_community_has_permission($translation->language, L10N_PERM_MODERATE_OTHERS); + } } /** @@ -1310,15 +1359,6 @@ function l10n_community_project_uri_by_title($title) { return db_result(db_query("SELECT uri FROM {l10n_community_project} WHERE title = '%s'", $title)); } -/** - * Check whether $suggestion is duplicate for $sid in $langcode. - */ -function l10n_community_is_duplicate($suggestion, $sid, $langcode) { - // Use BINARY matching to avoid marking case-corrections as duplicate. - // Matches everything active, regardless of being translations or suggestions. - return (bool) db_result(db_query("SELECT s.sid FROM {l10n_community_string} s LEFT JOIN {l10n_community_translation} t ON s.sid = t.sid WHERE t.translation = BINARY '%s' AND t.is_active = 1 AND t.language = '%s' AND s.sid = %d", $suggestion, $langcode, $sid)); -} - // = Theme functions =========================================================== /** @@ -1326,16 +1366,6 @@ function l10n_community_is_duplicate($suggestion, $sid, $langcode) { */ function l10n_community_theme($existing, $type, $theme, $path) { return array( - // l10n_community.module - 'l10n_community_button' => array( - 'arguments' => array('type' => NULL, 'class' => NULL, 'extras' => ''), - ), - 'l10n_community_strings' => array( - 'arguments' => array('items' => NULL, 'form' => TRUE), - ), - 'l10n_community_copy_button' => array( - 'arguments' => array(), - ), // pages.inc 'l10n_community_progress_columns' => array( 'arguments' => array('sum' => NULL, 'translated' => NULL, 'has_suggestion' => NULL), @@ -1350,12 +1380,27 @@ function l10n_community_theme($existing, $type, $theme, $path) { 'l10n_community_filter_form' => array( 'arguments' => array('form' => NULL), ), - 'l10n_community_translate_form' => array( - 'arguments' => array('form' => NULL), + 'l10n_community_translate_translation' => array( + 'arguments' => array('element' => NULL), + ), + 'l10n_community_translate_actions' => array( + 'arguments' => array('element' => NULL), ), 'l10n_community_in_context' => array( 'arguments' => array('source' => NULL), ), + 'l10n_community_translate_radio' => array( + 'arguments' => array('element' => NULL), + ), + 'l10n_community_translate_source' => array( + 'arguments' => array('element' => NULL), + ), + 'l10n_community_translate_translation_list' => array( + 'arguments' => array('element' => NULL), + ), + 'l10n_community_translate_table' => array( + 'arguments' => array('element' => NULL), + ), // l10n_community.admin.inc 'l10n_community_admin_projects_form' => array( 'arguments' => array('form' => NULL), @@ -1371,77 +1416,6 @@ function l10n_community_theme($existing, $type, $theme, $path) { } /** - * Theme a textual button. - * - * Text values are centralized here so it is easy to change. - */ -function theme_l10n_community_button($type, $class, $extras = '') { - switch ($type) { - case 'translate': - $text = t('Translate'); - break; - case 'lookup': - $text = t('Information'); - break; - case 'edit': - // Source string and translation edit field. - $text = t('Edit'); - break; - case 'has-suggestion': - case 'has-no-suggestion': - case 'untranslated': - // Star in a filled circle. - $text = t('Suggestions'); - break; - case 'approve': - // Checkmark. - $text = t('Approve'); - break; - case 'decline': - // Checkmark. - $text = t('Decline'); - break; - case 'save': - // Save button. - $text = t('Save'); - break; - case 'clear': - // Clear form button. - $text = t('Clear'); - break; - } - return ' '. $text .''; -} - -/** - * Theme a list of translatable strings. Adds a copy button to each string - * for quickly copying its source text into a translation form. - */ -function theme_l10n_community_strings($items, $form = TRUE) { - $output = ""; - return $output; -} - -/** - * Copy button for string values. - */ -function theme_l10n_community_copy_button() { - return theme('l10n_community_button', 'edit', 'l10n-community-copy'); -} - -/** * Format string for display. Takes plurals into account. */ function l10n_community_format_string($value, $rich_markup = TRUE) { diff --git l10n_community/translate.inc l10n_community/translate.inc index 8d07552..b099c2d 100644 --- l10n_community/translate.inc +++ l10n_community/translate.inc @@ -6,50 +6,7 @@ * Translation view and editing pages for localization community. */ -// = Translation interface hub ================================================= - -/** - * Menu callback for the translation pages. - * - * Displays a translation view or translation edit page depending - * on permissions. If no strings are found, an error is printed. - * - * @param $langcode - * Language code, for example 'hu', 'pt-br', 'de', 'it'. - */ -function l10n_community_translate_page($langcode = NULL, $mode = 'view') { - - // Add missing breadcrumb. - drupal_set_breadcrumb( - array( - l(t('Home'), NULL), - l(t('Translate'), 'translate') - ) - ); - - $languages = l10n_community_get_languages(); - $perm = l10n_community_get_permission($langcode); - - $filters = l10n_community_build_filter_values($_GET); - $output = drupal_get_form('l10n_community_filter_form', $filters); - - $strings = l10n_community_get_strings($languages[$langcode]->language, $filters, $filters['limit']); - if (!count($strings)) { - drupal_set_message(t('No strings found with this filter. Try adjusting the filter options.')); - } - elseif (!($perm & L10N_PERM_SUGGEST) || $mode == 'view') { - // For users without permission to translate or suggest, display the view. - drupal_set_title(t('@language translations', array('@language' => $languages[$langcode]->name))); - $output .= l10n_community_translate_view($strings, $languages[$langcode], $filters); - } - else { - // For users with some permission, display the form. - drupal_add_js(drupal_get_path('module', 'l10n_community') .'/l10n_community.js'); - drupal_set_title(t('Translate to @language', array('@language' => $languages[$langcode]->name))); - $output .= drupal_get_form('l10n_community_translate_form', $strings, $languages[$langcode], $filters, $perm); - } - return $output; -} +module_load_include('inc', 'l10n_community', 'api'); // = Filter form handling ====================================================== @@ -74,7 +31,7 @@ function l10n_community_filter_form(&$form_state, $filters, $limited = FALSE) { L10N_STATUS_NO_SUGGESTION => t('Has no suggestion'), L10N_STATUS_HAS_SUGGESTION => t('Has suggestion'), ); - + $form['project'] = array( '#title' => t('Project'), '#default_value' => isset($filters['project']) ? $filters['project']->title : '', @@ -190,386 +147,431 @@ function l10n_community_filter_form_submit($form, &$form_state) { } } -// = Translation viewer ======================================================== - /** - * Form for translations display. - * - * @param $strings - * Array of string objects to display on the page. - * @param $language - * Language object corresponding to the page displayed. - * @param $filters - * Filters used to present this listing view. + * Theme function for l10n_community_filter_form. */ -function l10n_community_translate_view($strings = array(), $language = NULL, $filters = array()) { - $output = ''; - $rows = array(); - foreach ($strings as $string) { - $row = array(); - // Source display - $source = l10n_community_format_string($string->value); - $source .= theme('l10n_community_in_context', $string); - $row[] = array('data' => $source, 'class' => 'source'); - - // Translation display. - if (!empty($string->translation)) { - if (strpos($string->value, chr(0)) !== FALSE) { - $translations = explode(chr(0), l10n_community_format_text($string->translation)); - // Fill in any missing items, so it is shown that not all items are done. - if (count($translations) < $language->plurals) { - $translations = array_merge($translations, array_fill(0, count($translations) - $language->plurals, '')); - } - $translation = theme('item_list', $translations); - } - else { - $translation = l10n_community_format_text($string->translation); - } - $row[] = $translation; - } - else { - $row[] = ''; +function theme_l10n_community_filter_form($form) { + $row = array(); + $labels = array(); + // Only display these elements in distinct table cells + $elements = array('project', 'release', 'context', 'status', 'author', 'search', 'limit'); + foreach ($form as $id => &$element) { + if (in_array($id, $elements)) { + $labels[] = $element['#title']; + unset($element['#title']); + $row[] = drupal_render($element); } - $rows[] = $row; } - $output .= ($pager = theme('pager', NULL, $filters['limit'], 0)); - $output .= theme('table', array(t('Source Text'), t('Translations')), $rows, array('class' => 'l10n-server-translate')); - $output .= $pager; - $output = "
". $output ."
"; - return $output; + // Fill in the rest of the header above the buttons. + $labels[] = ''; + // Display the rest of the form in the last cell + $row[] = array('data' => drupal_render($form), 'class' => 'last'); + return theme('table', $labels, array($row), array('class' => 'l10n-server-filter')); } -// = Translation editor ======================================================== +// = Translation view ========================================================== /** - * Translation web interface. - * - * @param $strings - * Array of string objects to display. - * @param $language - * Language object. - * @param $filters - * Filters used to present this editing view. - * @param $perm - * Community permission level of user watching the page. + * Menu callback: List translations and suggestions */ -function l10n_community_translate_form(&$form_state, $strings = array(), $language = NULL, $filters = array(), $perm = L10N_PERM_SUGGEST) { - - if (isset($_GET['page'])) { - // Ensure that we keep all filter values, even the page number, so - // after submission, the same page can be shown. - $filters['page'] = (int) $_GET['page']; - } +function l10n_community_translate_page($langcode) { + drupal_add_css(drupal_get_path('module', 'l10n_community') .'/editor.css'); + drupal_add_js(drupal_get_path('module', 'l10n_community') .'/jquery.worddiff.js'); + drupal_add_js(drupal_get_path('module', 'l10n_community') .'/editor.js'); - $form = array( - '#tree' => TRUE, - '#redirect' => array($_GET['q'], l10n_community_flat_filters($filters)) - ); - $form['pager_top'] = array( - '#weight' => -10, - '#value' => ($pager = theme('pager', NULL, $filters['limit'], 0)), - ); - $form['pager_bottom'] = array( - '#weight' => 10, - '#value' => $pager, - ); - // Keep language code and URI in form for further reference. - $form['langcode'] = array( - '#type' => 'value', - '#value' => $language->language - ); - $form['project'] = array( - '#type' => 'value', - '#value' => isset($project) ? $project->uri : NULL - ); + $language = l10n_community_get_language($langcode); + $filters = l10n_community_build_filter_values($_GET); + $strings = l10n_community_get_strings($language->language, $filters, $filters['limit']); - foreach ($strings as $string) { - $form[$string->sid] = array( - '#tree' => TRUE, - ); + // Set the most appropriate title. + if ($filters['project']) { + drupal_set_title(t('Translate %project to @language', array('%project' => $filters['project']->title, '@language' => $language->name))); + } + else { + drupal_set_title(t('Translate to @language', array('@language' => $language->name))); + } - // A toolbox which displays action icons on each string editor fieldset. - $toolbox = theme('l10n_community_button', 'translate', 'l10n-translate active'); - $toolbox .= theme('l10n_community_button', 'lookup', 'l10n-lookup'); - $toolbox .= $string->has_suggestion ? theme('l10n_community_button', 'has-suggestion', 'l10n-suggestions') : ""; - $toolbox = "
$toolbox
"; - $form[$string->sid]['toolbox'] = array( - '#type' => 'markup', - '#value' => $toolbox, - ); - $form[$string->sid]['messagebox'] = array( - '#type' => 'markup', - '#value' => "
", - ); + // Add the filter form. + $output = drupal_get_form('l10n_community_filter_form', $filters); - $is_plural = strpos($string->value, "\0"); - // Multiple source strings if we deal with plurals. The form item and - // consequently the JavaScript strings identifiers are the sid and then - // the index of the plural being displayed. - $string_parts = explode(chr(0), $string->value); - foreach ($string_parts as $delta => &$part) { - $part = l10n_community_format_text($part, $string->sid, (count($string_parts) > 1) ? $delta : NULL); - } - $source = theme('l10n_community_strings', $string_parts); - $source .= theme('l10n_community_in_context', $string); + // Output the actual strings. + if (!count($strings)) { + drupal_set_message(t('No strings found with this filter. Try adjusting the filter options.')); + } + else { + $output .= drupal_get_form('l10n_community_translate_form', $language, $filters, $strings); + } - $form[$string->sid]['source'] = array( - '#type' => 'item', - '#value' => $source, - ); + return $output; +} - $translated = !empty($string->translation); - $form[$string->sid]['translation'] = array( - '#type' => 'item', - // Hide editing controls of translated stuff to save some space and guide user eyes. - '#prefix' => '', - ); +/** + * Form callback: List translations and suggestions. + * + * @param $form_state + * The form state array. + * @param $language + * A language object. + * @param $filters + * An array of filters applied to the strings. + * @param $strings + * The strings to render. + */ +function l10n_community_translate_form(&$form_state, $language, $filters, $strings) { + $permission = l10n_community_get_permission($language->language); + $pager = theme('pager', NULL, $filters['limit'], 0); + $redirect_url = $_GET; + unset($redirect_url['q']); - if ($is_plural) { + $form = array( + '#submit' => array('l10n_community_translate_submit'), + '#redirect' => array($_GET['q'], $redirect_url), + 'langcode' => array('#type' => 'value', '#value' => $language->language), + 'pager_top' => array('#weight' => -10, '#value' => $pager), + 'submit_top' => array('#type' => 'submit', '#value' => t('Save changes'), '#access' => $permission & L10N_PERM_SUGGEST), + 'strings' => array('#tree' => TRUE, '#theme' => 'l10n_community_translate_table'), + 'submit' => array('#type' => 'submit', '#value' => t('Save changes'), '#access' => $permission & L10N_PERM_SUGGEST), + 'pager_bottom' => array('#weight' => 10, '#value' => $pager), + ); - // Dealing with a string with plural versions. - if ($translated) { - // Add translation form element with all plural versions. - $translations = explode("\0", $string->translation); - $string_parts = array(); - for ($i = 0; $i < $language->plurals; $i++) { - $target = $string->sid .'-'. $i; - $string_parts[] = l10n_community_format_text($translations[$i], $string->sid, $i); - } - $form[$string->sid]['translation_existing'] = array( - '#type' => 'item', - '#value' => theme('l10n_community_strings', $string_parts), - ); - } + foreach ($strings as $string) { + $form['strings'][$string->sid] = _l10n_community_translate_string($form_state, $string, $language, $permission); + } - $string_parts = explode(chr(0), $string->value); + return $form; +} - for ($i = 0; $i < $language->plurals; $i++) { - $target = $string->sid .'-'. $i; - if ($translated) { - // Already translated so we ask for new translation or suggestion. - $description = !($perm & L10N_PERM_MODERATE_OWN) ? t('New suggestion for variant #%d', array('%d' => $i)) : t('New translation for variant #%d', array('%d' => $i)); - } - else { - // Not translated yet, so we ask for initial translation or suggestion. - $description = !($perm & L10N_PERM_MODERATE_OWN) ? t('Suggestion for variant #%d', array('%d' => $i)) : t('Translation for variant #%d', array('%d' => $i)); - } +/** + * Return a marked-up string. + */ +function _l10n_community_translate_render_strings($strings, $empty = '') { + if ($empty) { + $empty = ' data-empty="'. check_plain($empty) .'"'; + } + return "". implode("
", array_map('check_plain', $strings)) .''; +} - // Include editing area for each plural variant. - $source_index = ($i > 0 ? 1 : 0); - $form[$string->sid]['translation']['value'][$i] = array( - // Use textarea for long and multiline strings. - '#type' => ((strlen($string_parts[$source_index]) > 45) || (count(explode("\n", $string_parts[$source_index])) > 1)) ? 'textarea' : 'textfield', - '#description' => $description, - '#rows' => 1, - '#id' => 'l10n-community-translation-'. $target, - ); - } - } +/** + * Creates the form fragment for a source string. + */ +function _l10n_community_translate_string(&$form_state, $source, $language, $permission) { + // Normalize empty default translation. + if (!$source->translation) { + $source->tid = '0'; + $source->translation = array(t('(not translated)')); + $source->is_active = '1'; + $source->is_suggestion = '0'; + } + else { + $source->translation = l10n_community_unpack_string($source->translation); + } - // Dealing with a simple string (no plurals). + $source->value = l10n_community_unpack_string($source->value); - else { - if ($translated) { - $form[$string->sid]['translation_existing'] = array( - '#type' => 'item', - '#value' => theme('l10n_community_strings', array(l10n_community_format_text($string->translation, $string->sid))), - ); - } - $form[$string->sid]['translation']['value'] = array( - // Use textarea for long and multiline strings. - '#type' => ((strlen($string->value) > 45) || (count(explode("\n", $string->value)) > 1)) ? 'textarea' : 'textfield', - // Provide accurate title based on previous data and permission. - '#description' => $translated ? (!($perm & L10N_PERM_MODERATE_OWN) ? t('Add a new suggestion') : t('Add a new translation')) : (!($perm & L10N_PERM_MODERATE_OWN) ? t('Suggestion') : ''), - '#rows' => 4, - '#resizable' => FALSE, - '#cols' => NULL, - '#size' => NULL, - '#id' => 'l10n-community-translation-'. $string->sid, - ); - if (strlen($string->value) > 200) { - $form[$string->sid]['translation']['value']['#rows'] = floor(strlen($string->value) * .03); - $form[$string->sid]['translation']['value']['#resizable'] = TRUE; - } - } + $form = array( + '#string' => $source, + '#langcode' => $language->language, + 'source' => array( + 'string' => array('#value' => _l10n_community_translate_render_strings($source->value)), + ), + ); - // Add AJAX saving buttons - $form[$string->sid]['translation']['save'] = array( - '#prefix' => "", - '#value' => theme('l10n_community_button', 'save', 'l10n-save'), - '#type' => 'markup', - ); - $form[$string->sid]['translation']['clear'] = array( - '#suffix' => "", - '#value' => theme('l10n_community_button', 'clear', 'l10n-clear'), - '#type' => 'markup', + if ($permission & L10N_PERM_SUGGEST) { + $form['source']['edit'] = array( + '#value' => t('Edit Copy'), + '#prefix' => '', ); + } - if (!($perm & L10N_PERM_MODERATE_OWN)) { - // User with suggestion capability only, record this. - $form[$string->sid]['translation']['is_suggestion'] = array( - '#type' => 'value', - '#value' => TRUE - ); - } - else { - // User with full privileges, offer option to submit suggestion. - $form[$string->sid]['translation']['is_suggestion'] = array( - '#title' => t('Suggestion for discussion'), - '#type' => 'checkbox', - ); + // Add the current string (either a approved translation or a mock object + // for the "untranslated" string). + $form[$source->tid] = _l10n_community_translate_translation($form_state, $source, $permission, $source); + + // When there are suggestions, load them from the database. + if ($source->has_suggestion) { + $result = db_query("SELECT t.tid, t.sid, t.translation, t.uid_entered, t.time_entered, t.is_active, t.is_suggestion, u.name as username FROM {l10n_community_translation} t LEFT JOIN {users} u ON u.uid = t.uid_entered WHERE t.language = '%s' AND t.sid = %d AND t.is_active = 1 AND t.is_suggestion = 1 ORDER BY t.time_entered", $language->language, $source->sid); + while ($suggestion = db_fetch_object($result)) { + $suggestion->translation = l10n_community_unpack_string($suggestion->translation); + // Add the suggestion to the list. + $form[$suggestion->tid] = _l10n_community_translate_translation($form_state, $suggestion, $permission, $source); } } - // Add all strings for copy-pasting and some helpers. - drupal_add_js( - array( - 'l10n_lookup_help' => t('Show detailed information.'), - 'l10n_approve_error' => t('There was an error approving this suggestion. You might not have permission or the suggestion id was invalid.'), - 'l10n_approve_confirm' => t('!icon Suggestion approved.', array('!icon' => '✔')), + // If the user may add new suggestions, display a textarea. + if ($permission & L10N_PERM_SUGGEST) { + $textarea = _l10n_community_translate_translation_textarea($source, $language); + $form[$textarea->tid] = _l10n_community_translate_translation($form_state, $textarea, $permission, $source); + } - 'l10n_decline_error' => t('There was an error declining this suggestion. You might not have permission or the suggestion id was invalid.'), - 'l10n_decline_confirm' => t('Suggestion declined.'), + return $form; +} - 'l10n_details_callback' => url('translate/details/'. $language->language .'/'), - 'l10n_suggestions_callback' => url('translate/suggestions/'. $language->language .'/'), - 'l10n_approve_callback' => url('translate/approve/'), - 'l10n_decline_callback' => url('translate/decline/'), - 'l10n_form_token_path' => variable_get('clean_url', '0') ? '?form_token=' : '&form_token=', - 'l10n_num_plurals' => $language->plurals - ), - 'setting' - ); +/** + * Build mock object for new textarea. + */ +function _l10n_community_translate_translation_textarea($source, $language) { + global $user; - // Let the user submit the form. - $form['submit'] = array( - '#type' => 'submit', - '#value' => !($perm & L10N_PERM_MODERATE_OWN) ? t('Save suggestions') : t('Save translations') + return (object)array( + 'sid' => $source->sid, + 'tid' => 'new', + 'translation' => array_fill(0, count($source->value), ''), + 'is_active' => '1', + 'is_suggestion' => '1', + 'uid_entered' => $user->uid, ); +} - $form['#theme'] = 'l10n_community_translate_form'; +/** + * Generates the byline containing meta information about a string. + */ +function l10n_community_translate_byline($string) { + $params = array( + '!author' => theme('username', (object)array('name' => $string->username, 'uid' => $string->uid_entered)), + '@date' => format_date($string->time_entered), + '@ago' => t('@time ago', array('@time' => format_interval(time() - $string->time_entered))), + ); - return $form; + if (!empty($string->uid_approved) && $string->uid_entered === $string->uid_approved && $string->time_entered === $string->time_approved) { + return t('translated and approved by !author on @date', $params); + } + else { + $title = t('suggested by !author on @date', $params); + if (!empty($string->uid_approved)) { + $title .= '
'. t('approved by !author on @date', array( + '!author' => theme('username', (object)array('name' => $string->username_approved, 'uid' => $string->uid_approved)), + '@date' => format_date($string->time_approved), + '@ago' => t('@time ago', array('@time' => format_interval(time() - $string->time_approved))), + )); + } + return $title; + } } /** - * Save translations entered in the web form. + * Creates the form fragment for a translated string. */ -function l10n_community_translate_form_submit($form, &$form_state) { +function _l10n_community_translate_translation(&$form_state, $string, $permission, $source) { global $user; + $is_own = $user->uid == $string->uid_entered; + $is_active = $string->is_active && !$string->is_suggestion; + $is_new = $string->tid == 'new'; + $may_moderate = $permission & ($is_own ? L10N_PERM_MODERATE_OWN : L10N_PERM_MODERATE_OTHERS); + //$may_stabilize = $permission & ($is_own ? L10N_PERM_STABILIZE_OWN : L10N_PERM_STABILIZE_OTHERS); - $inserted = $updated = $unchanged = $suggested = $duplicates = $ignored = 0; + $form = array( + '#theme' => 'l10n_community_translate_translation', + 'original' => array('#type' => 'value', '#value' => $string), + ); - foreach ($form_state['values'] as $sid => $item) { - if (!is_array($item) || !isset($item['translation'])) { - // Skip, if we don't have translations in this form item, - // which means this is some other form value. - continue; - } + $form['active'] = array( + '#type' => 'radio', + '#theme' => 'l10n_community_translate_radio', + '#title' => _l10n_community_translate_render_strings($string->translation, $is_new ? t('(empty)') : FALSE), + '#return_value' => $string->tid, + '#default_value' => $is_active ? $string->tid : NULL, + '#parents' => array('strings', $string->sid, 'active'), + '#disabled' => !$may_moderate && !$is_active, + '#attributes' => array('class' => 'selector'), + ); - $source_string = db_result(db_query('SELECT value FROM {l10n_community_string} WHERE sid = %d', $sid)); - $text = ''; - if (is_string($item['translation']['value']) && strlen(trim($item['translation']['value']))) { - // Single string representation: simple translation. - $text = l10n_community_trim($item['translation']['value'], $source_string); + if ($string->tid) { + if ($may_moderate && $string->tid != 'new') { + $form['declined'] = array( + '#type' => 'checkbox', + '#title' => t('Declined'), + '#default_value' => !($string->is_active || $string->is_suggestion), + ); } - if (is_array($item['translation']['value'])) { - // Array -> plural variants are provided. Join them with a NULL separator. - $text = join("\0", $item['translation']['value']); - if (trim($text) == '') { - // If the whole string only contains NULL bytes, empty the string, so - // we don't save an empty translation. Otherwise the NULL bytes need - // to be there, so we know plural variant indices. - $text = ''; - } + // if ($may_stabilize) { + // $form['stable'] = array( + // '#type' => 'checkbox', + // '#title' => t('Stable'), + // '#default_value' => FALSE, // $string->is_stable, + // ); + // } + if ($string->tid == 'new') { + $form['value'] = array_fill(0, count($source->value), array( + '#type' => 'textarea', + '#cols' => 60, + '#rows' => 3, + '#default_value' => t(''), + )); } + else { + if ($permission & L10N_PERM_SUGGEST) { + $form['edit'] = array( + '#value' => t('Edit Copy'), + '#prefix' => '', + ); + } + if (isset($string->username)) { + $title = l10n_community_translate_byline($string); - if (!empty($text)) { - // Check for duplicate translation or suggestion. - if (l10n_community_is_duplicate($text, $sid, $form_state['values']['langcode'])) { - $duplicates++; - continue; + $form['author'] = array( + '#value' => $title, + ); } + } + } - // We have some string to save. - l10n_community_target_save( - $sid, $text, $form_state['values']['langcode'], $user->uid, - ($item['translation']['is_suggestion'] == TRUE), - $inserted, $updated, $unchanged, $suggested - ); + return $form; +} + +function theme_l10n_community_translate_actions($element) { + $actions = ''; + foreach (array('declined', /*'stable', */'edit') as $type) { + if (isset($element[$type])) { + $actions .= '
  • '. drupal_render($element[$type]) .'
  • '; } } + if (!empty($actions)) { + return '
      '. $actions .'
    '; + } + else { + return ''; + } +} + +function theme_l10n_community_translate_translation($element) { + if (!isset($element['#attributes']['class'])) { + $element['#attributes']['class'] = ''; + } + $element['#attributes']['class'] .= ' clearfix translation'; + + switch ($element['active']['#return_value']) { + case 'new': + $element['#attributes']['class'] .= ' new-translation'; + break; + case '0': + $element['#attributes']['class'] .= ' no-translation'; + // Fallthrough. + default: + if ($element['active']['#value'] !== '') { + $element['#attributes']['class'] .= ' is-active default'; + } + } + + $output = ''; + $output .= theme('l10n_community_translate_actions', $element); + $output .= drupal_render($element['active']); + + if (isset($element['author'])) { + $output .= '
    '. drupal_render($element['author']) .'
    '; + } + + if (isset($element['value'])) { + $output .= drupal_render($element['value']); + } - // Inform user about changes made to the database. - l10n_community_update_message($inserted, $updated, $unchanged, $suggested, $duplicates, $ignored); + return $output . ''; } -// = Theme functions =========================================================== +function theme_l10n_community_translate_radio($element) { + _form_set_class($element, array('form-radio')); + $output = ''; + + if (isset($element['#title'])) { + $output .= ''; + } -/** - * Theme function for l10n_community_filter_form. - */ -function theme_l10n_community_filter_form($form) { - $row = array(); - $labels = array(); - // Only display these elements in distinct table cells - $elements = array('project', 'release', 'context', 'status', 'author', 'search', 'limit'); - foreach ($form as $id => &$element) { - if (in_array($id, $elements)) { - $labels[] = $element['#title']; - unset($element['#title']); - $row[] = drupal_render($element); + return $output; +} + +function theme_l10n_community_translate_translation_list($element) { + $output = '
      '; + foreach (element_children($element) as $child) { + if (is_numeric($child) || $child == 'new') { + $output .= drupal_render($element[$child]); } } - // Fill in the rest of the header above the buttons. - $labels[] = ''; - // Display the rest of the form in the last cell - $row[] = array('data' => drupal_render($form), 'class' => 'last'); - return theme('table', $labels, array($row), array('class' => 'l10n-server-filter')); + $output .= '
    '; + + return $output; } -/** - * Theme function for l10n_community_translate_form. - */ -function theme_l10n_community_translate_form($form) { +function theme_l10n_community_translate_source($element) { + $output = theme('l10n_community_translate_actions', $element['source']); + $output .= ''; + $output .= theme('l10n_community_in_context', $element['#string']); + $output .= ''; + return $output; +} + +function theme_l10n_community_translate_table($element) { + $header = array( + t('Source Text'), + t('Translations'), + ); + $rows = array(); - $output = ''; + foreach (element_children($element) as $key) { + $rows[] = array( + array('class' => 'source', 'data' => theme('l10n_community_translate_source', $element[$key])), + array('class' => 'translation', 'data' => theme('l10n_community_translate_translation_list', $element[$key])), + ); + } - foreach ($form as $id => &$element) { - // if the form id is numeric, this form element is for editing a string - if (is_numeric($id)) { - $source_pane = drupal_render($element['source']); - $translation_pane = "
    "; - $translation_pane .= drupal_render($element['toolbox']); - $translation_pane .= "
    "; - $translation_pane .= !empty($element['translation_existing']) ? drupal_render($element['translation_existing']) : ''; - $translation_pane .= drupal_render($element['messagebox']); - $translation_pane .= drupal_render($element['translation']) .'
    '; - $translation_pane .= "
    "; - $translation_pane .= "
    "; - $row = array( - array( - 'data' => $source_pane, - 'class' => 'source', - 'id' => 'spane-'. $id, - ), - array( - 'data' => $translation_pane, - 'class' => 'translation', - 'id' => 'tpane-'. $id, - ), - ); - $rows[] = $row; - unset($form[$id]); + return theme('table', $header, $rows, array('class' => 'l10n-table')); +} + +function l10n_community_translate_submit($form, &$form_state) { + $langcode = $form_state['values']['langcode']; + + foreach ($form_state['values']['strings'] as $sid => $string) { + foreach ($string as $tid => $options) { + // Store new suggestion. + $empty_values = 0; + if (isset($options['value']) && is_array($options['value'])) { + foreach ($options['value'] as $key => $value) { + if ($value === t('')) { + $options['value'] = ''; + $empty_values++; + } + } + if ($tid === 'new' && count($options['value']) > $empty_values) { + $tid = l10n_community_add_suggestion($langcode, $sid, $options['value']); + if ($tid) { + l10n_community_counter('added'); + if ($string['active'] === 'new') { + $string['active'] = $tid; + } + } + } + } + + if (is_numeric($tid) && $tid > 0) { + if ($tid == $string['active']) { + if ($options['original']->is_suggestion) { + l10n_community_approve_string($langcode, $sid, $tid); + l10n_community_counter('approved'); + } + // if (/*!$options['original']->is_stable && */!empty($options['stable'])) { + // dsm('mark as stable'); + // l10n_community_counter('stabilized'); + // } + } + elseif (!empty($options['declined'])) { + // also remove stable flag! + l10n_community_counter($options['original']->is_suggestion ? 'suggestion_declined' : 'declined'); + l10n_community_decline_string($langcode, $sid, $tid); + } + } } } - $output .= drupal_render($form['pager_top']); - $output .= theme('table', array(t('Source Text'), t('Translations')), $rows, array('class' => 'l10n-server-translate')); - $output .= drupal_render($form); - return $output; + + l10n_community_update_message(); } +// = Miscellaneous ============================================================= + /** * Theme context information for source strings. * @@ -615,10 +617,12 @@ function l10n_community_get_strings($langcode, $filters, $pager = NULL) { $join = $join_args = $where = $where_args = array(); $sql = $sql_count = ''; - $select = "SELECT DISTINCT s.sid, s.value, s.context, t.tid, t.language, t.translation, t.uid_entered, t.uid_approved, t.time_entered, t.time_approved, t.has_suggestion, t.is_suggestion, t.is_active FROM {l10n_community_string} s"; + $select = "SELECT DISTINCT s.sid, s.value, s.context, t.tid, t.language, t.translation, t.uid_entered, t.uid_approved, t.time_entered, t.time_approved, t.has_suggestion, t.is_suggestion, t.is_active, u.name as username, u2.name as username_approved FROM {l10n_community_string} s"; $select_count = "SELECT COUNT(DISTINCT(s.sid)) FROM {l10n_community_string} s"; $join[] = "LEFT JOIN {l10n_community_translation} t ON s.sid = t.sid AND t.language = '%s' AND t.is_active = 1 AND t.is_suggestion = 0"; $join_args[] = $langcode; + $join[] = "LEFT JOIN {users} u ON u.uid = t.uid_entered"; + $join[] = "LEFT JOIN {users} u2 ON u2.uid = t.uid_approved"; // Add submitted by condition if (!empty($filters['author'])) { @@ -636,7 +640,7 @@ function l10n_community_get_strings($langcode, $filters, $pager = NULL) { // Release restriction. $where_args[] = $release; $where[] = 'l.rid = %d'; - } + } elseif ($project) { $where[] = "l.pid = %d"; $where_args[] = $project->pid; @@ -659,18 +663,18 @@ function l10n_community_get_strings($langcode, $filters, $pager = NULL) { // Restriction based on string status by translation / suggestions. $status_sql = ''; - if ($filters['status'] & L10N_STATUS_UNTRANSLATED) { + if (isset($filters['status']) && $filters['status'] & L10N_STATUS_UNTRANSLATED) { // We are doing a LEFT JOIN especially to look into the case, when we have nothing // to match in the translation table, but we still have the string. (We get our // records in the result set in this case). The translation field is empty or // NULL in this case, as we are not allowing NULL there and only saving an empty // translation if there are suggestions but no translation yet. $where[] = "(t.translation is NULL OR t.translation = '')"; - } + } elseif ($filters['status'] & L10N_STATUS_TRANSLATED) { $where[] = "t.translation != ''"; } - if ($filters['status'] & L10N_STATUS_HAS_SUGGESTION) { + if (isset($filters['status']) && $filters['status'] & L10N_STATUS_HAS_SUGGESTION) { // Note that we are not searching in the suggestions themselfs, only // the source and active translation values. The user interface underlines // that we are looking for strings which have suggestions, not the @@ -732,9 +736,9 @@ function l10n_community_build_filter_values($params, $suggestions = FALSE) { 'context' => isset($params['context']) ? (string) $params['context'] : 'all', 'limit' => (isset($params['limit']) && in_array($params['limit'], array(5, 10, 20, 30))) ? (int) $params['limit'] : 10, ); - - // The project can be a dropdown or text field depending on number of - // projects. So we need to sanitize its value. + + // The project can be a dropdown or text field depending on number of + // projects. So we need to sanitize its value. if (isset($params['project'])) { // Try to load project by uri or title, but give URI priority. URI is used // to shorten the URL and have simple redirects. Title is used if the @@ -756,6 +760,152 @@ function l10n_community_build_filter_values($params, $suggestions = FALSE) { } /** + * Ensures that there is a mock translation for a given language/string. + * + * @param $langcode + * The language to be checked for a mock translation. + * @param $sid + * The string ID that needs to have a mock translation. + */ +function l10n_community_mock_translation($langcode, $sid) { + if (!db_result(db_query("SELECT COUNT(*) FROM {l10n_community_translation} WHERE sid = %d AND language = '%s' AND is_suggestion = 0 AND is_active = 1", $sid, $langcode))) { + // Insert mock tuple that acts as placeholder. + db_query("INSERT INTO {l10n_community_translation} (sid, translation, language, uid_entered, time_entered, is_suggestion, is_active) VALUES (%d, '', '%s', 0, 0, 0, 1)", $sid, $langcode); + } +} + +/** + * Adds a suggestion to a language/string. + * + * @param $langcode + * The language of the new translation. + * @param $sid + * The string ID for which a new translation should be added. + * @param $translation + * An array of strings which constitute the new translation. + */ +function l10n_community_add_suggestion($langcode, $sid, $translation) { + global $user; + + // Load source string and adjust translation whitespace based on source. + $source_string = db_result(db_query('SELECT value FROM {l10n_community_string} WHERE sid = %d', $sid)); + $translation = l10n_community_pack_string($translation); + $translation = l10n_community_trim($translation, $source_string); + + // Don't store empty translations. + if ($translation === '') { + return NULL; + } + + // Look for an existing active translation, if any. + $existing = db_fetch_object(db_query("SELECT tid FROM {l10n_community_translation} WHERE sid = %d AND language = '%s' AND translation = '%s'", $sid, $langcode, $translation)); + if (!empty($existing)) { + // The translation is already in the db. Make it an active suggestion again. + db_query("UPDATE {l10n_community_translation} SET is_suggestion = 1, is_active = 1 WHERE tid = %d", $existing->tid); + $tid = $existing->tid; + } + else { + // This is a new translation. + l10n_community_mock_translation($langcode, $sid); + // Insert the new suggestion. + db_query("INSERT INTO {l10n_community_translation} (sid, translation, language, uid_entered, time_entered, is_suggestion, is_active) VALUES (%d, '%s', '%s', %d, %d, 1, 1)", $sid, $translation, $langcode, $user->uid, time()); + $tid = db_last_insert_id('l10n_community_translation', 'tid'); + } + + // Mark the existing or mock translation has having suggestions. + l10n_community_update_suggestion_status($langcode, $sid); + + return $tid; +} + +/** + * Marks a translation as declined. + * + * @param $langcode + * The language of the declined translation. + * @param $sid + * The string ID the translation belongs to. + * @param $tid + * The translation ID of the translation. + */ +function l10n_community_decline_string($langcode, $sid, $tid) { + // Mark this translation as inactive. + db_query("UPDATE {l10n_community_translation} SET is_suggestion = 0, is_active = 0 WHERE tid = %d", $tid); + + // Make sure the mock translation is there in case we declined the active translation. + l10n_community_mock_translation($langcode, $sid); + l10n_community_update_suggestion_status($langcode, $sid); +} + +/** + * Updates the has_suggestion flag for the active translation. + * + * @param $langcode + * The language of the string. + * @param $sid + * The string ID that should be updated. + */ +function l10n_community_update_suggestion_status($langcode, $sid) { + // Let's see if we have any suggestions remaining in this language. + $count = db_result(db_query("SELECT COUNT(*) FROM {l10n_community_translation} + WHERE sid = %d AND is_suggestion = 1 AND is_active = 1 AND language = '%s'", $sid, $langcode)); + + // Update the status according to the number of suggestions. + db_query("UPDATE {l10n_community_translation} SET has_suggestion = %d + WHERE sid = %d AND is_suggestion = 0 AND is_active = 1 AND language = '%s'", $count ? 1 : 0, $sid, $langcode); +} + +/** + * Marks a translation as approve. + * + * @param $langcode + * The language of the approved translation. + * @param $sid + * The string ID the translation belongs to. + * @param $tid + * The translation ID of the translation. + */ +function l10n_community_approve_string($langcode, $sid, $tid) { + global $user; + + // Remove placeholder translation record (which was there if + // first came suggestions, before an actual translation). + db_query("DELETE FROM {l10n_community_translation} WHERE sid = %d AND translation = '' AND language = '%s'", $sid, $langcode); + + // Make the existing approved string a suggestion. + db_query("UPDATE {l10n_community_translation} SET is_suggestion = 1 WHERE sid = %d AND language = '%s' AND is_suggestion = 0 AND is_active = 1", $sid, $langcode); + + // Mark this exact suggestion as active, and set approval time. + db_query("UPDATE {l10n_community_translation} SET time_approved = %d, uid_approved = %d, is_suggestion = 0, is_active = 1 WHERE tid = %d;", time(), $user->uid, $tid); + l10n_community_update_suggestion_status($langcode, $sid); +} + +/** + * Unpacks a string as retrieved from the database. + * + * @param $string + * The string with separation markers (NULL byte) + * @return + * An array of strings with one element for each plural form in case of + * a plural string, or one element in case of a regular string. + */ +function l10n_community_unpack_string($string) { + return explode("\0", $string); +} + +/** + * Packs a string for storage in the database. + * + * @param $string + * An array of strings. + * @return + * A packed string with NULL bytes separating each string. + */ +function l10n_community_pack_string($strings) { + return implode("\0", $strings); +} + +/** * Replace complex data filters (objects or arrays) with string representations. * * @param $filters @@ -771,3 +921,49 @@ function l10n_community_flat_filters($filters) { } return $filters; } + + +// = AJAX callbacks ============================================================ + +/** + * Return a HTML list of projects, releases and counts of where strings + * appear in the managed projects. + * + * We could have been provided much more information, but usability should + * also be kept in mind. It is possible to investigate hidden information + * sources though, like tooltips on the release titles presented. + * + * This callback is invoked from JavaScript and is used as an AHAH provider. + * + * @param $langcode + * Language code. + * @param $sid + * Source string id. + */ +function l10n_community_string_details($langcode = NULL, $sid = 0) { + // Prevent devel module information. + $GLOBALS['devel_shutdown'] = FALSE; + + // List of project releases, where this string is used. + $result = db_query('SELECT l.pid, p.title project_title, l.rid, r.title release_title, COUNT(l.lineno) as occurance_count FROM {l10n_community_line} l INNER JOIN {l10n_community_project} p ON l.pid = p.pid INNER JOIN {l10n_community_release} r ON l.rid = r.rid WHERE l.sid = %d AND p.status = 1 GROUP BY l.rid ORDER BY l.pid, l.rid', $sid); + + $list = array(); + $output = array(); + $previous_project = ''; + while ($instance = db_fetch_object($result)) { + if ($instance->project_title != $previous_project) { + if (!empty($list)) { + $output[] = join(', ', $list); + } + $list = array(''. $instance->project_title .': '. $instance->release_title .' ('. $instance->occurance_count .')'); + } + else { + $list[] = $instance->release_title .' ('. $instance->occurance_count .')'; + } + $previous_project = $instance->project_title; + } + $output[] = join(', ', $list); + print ''. t('Used in:') .''. theme('item_list', $output); + + exit; +}