Index: l10n_community/l10n_community.js =================================================================== RCS file: l10n_community/l10n_community.js diff -N l10n_community/l10n_community.js --- l10n_community/l10n_community.js 15 Oct 2009 08:46:40 -0000 1.1.2.8.2.5 +++ /dev/null 1 Jan 1970 00:00:00 -0000 @@ -1,220 +0,0 @@ -// $Id: l10n_community.js,v 1.1.2.8.2.5 2009/10/15 08:46:40 goba Exp $ - -l10nCommunity = {}; - -l10nCommunity.switchPanes = function(elem, id) { - if ($(elem).parents('.translation').find('.'+id).css('display') == 'none') { - // execute animation once per translation - var once = 0; - $(elem).parents('.translation').find('.pane:not(.'+id+')').each(function() { - if ($(this).css('display') == 'block') { - $(this).slideUp('fast', function() { - if (once == 0) { - once = 1; - $(this).parents('.translation').find('.'+id).slideDown('fast'); - } - }); - } - }); - } - $(elem).parents('.toolbox').find('span.l10n-button').removeClass('active'); - $(elem).addClass('active'); -} - -/** - * Initialize action images: toolboxes and string copy buttons. - */ -l10nCommunity.init = function() { - // Only attempt to register events if form exists - if ($('#l10n-community-translate-form').size() > 0) { - // When the copy button is clicked, copy the original string value to the - // translation field for the given strings. Relations are maintained with - // the string ideitifiers. - $('span.l10n-community-copy').click(function() { - l10nCommunity.copyString(this); - ;}) - - // Screw AJAX submit -- we'll just hard submit the form for now - $('span.l10n-save').click(function() { - $('#l10n-community-translate-form').submit(); - }); - - // Clear sibling text fields - $('span.l10n-clear').click(function() { - $(this).parents('.translation').find('input.form-text, textarea').val(''); - }); - - - $('#l10n-community-translate-form .l10n-translate').click(function() { - // switch display panes - l10nCommunity.switchPanes(this, 'translate'); - }); - - $('#l10n-community-translate-form .l10n-lookup').click(function() { - // switch display panes - var elem = this; - if ($(this).is(".active")) { - // Switch back to editing form if already clicked. Convenience feature, - // so that you don't need to move your mouse to switch back. - var parent = $(elem).parents('.translation'); - var tool = $('.l10n-translate', parent); - l10nCommunity.switchPanes(tool, 'translate'); - return; - } - var sid = $(this).parents('.translation').attr('id').substring(6); - $.get(Drupal.settings.l10n_details_callback + sid, null, function(data) { - $('#tpane-' + sid + ' .lookup').empty().append(data); - l10nCommunity.switchPanes(elem, 'lookup'); - }); - }); - - $('#l10n-community-translate-form .l10n-suggestions').click(function() { - // switch display panes - var elem = this; - if ($(this).is(".active")) { - // Switch back to editing form if already clicked. Convenience feature, - // so that you don't need to move your mouse to switch back. - var parent = $(elem).parents('.translation'); - var tool = $('.l10n-translate', parent); - l10nCommunity.switchPanes(tool, 'translate'); - return; - } - var sid = $(this).parents('.translation').attr('id').substring(6); - $.get(Drupal.settings.l10n_suggestions_callback + sid, null, function(data) { - $('#tpane-' + sid + ' .suggestions').empty().append(data); - l10nCommunity.switchPanes(elem, 'suggestions'); - var suggestions = $('#tpane-' + sid + ' .suggestions'); - $('span.l10n-community-copy', suggestions).click(function() { - l10nCommunity.copyString(this); - }); - // Hide the has-suggestion marker if we don't have suggestions anymore. - // Could happen if we declined the last suggestion and reloading. - $('#l10n-community-editor-' + sid + ' .l10n-no-suggestions').parent().parent().find('.l10n-has-suggestion').hide(); - }); - }); - } -} - -l10nCommunity.copyString = function(elem) { - var item = $(elem).parents('li').find('div.string > div'); - var parentlist = $(item).parents('ul.l10n-community-strings'); - var original = $('.original', item).text(); - var sid = item.attr('class').substring(7); - - if (sid.indexOf('-') > 0) { - // Copying orignal plural string or active plural translation. We should - // get the original sid prefix and then copy the strings to the textareas. - sid = sid.split('-'); - sid = sid[0]; - for (i = 0; i < Drupal.settings.l10n_num_plurals; i++) { - source_index = (i > 0 ? 1 : 0); - $('#l10n-community-translation-'+ sid +'-'+ i).val($('.string-'+ sid +'-'+ source_index +' .original', parentlist).text()); - } - } - else if (original.indexOf("; ") > 0) { - // Copying plural suggestions from the suggestion list. This has the special - // "; " delimiter (which we suppose does not exist in source strings). - var strings = original.split("; "); - for (string in strings) { - $('#l10n-community-translation-'+ sid +'-'+ string).val(strings[string]); - } - } - else { - // Otherwise simple string. Just copy over the original string. - $('#l10n-community-translation-' + sid).val(original); - } - - // Show the editing controls. - $('#l10n-community-wrapper-' + sid).show(); - - // Switch to translate pane. The pane is in the translation column, so - // if we are in the source column, we need to switch columns. - var parent = $(elem).parents('td.source, td.translation'); - if (parent.get(0).className == 'source') { - parent = $(parent.get(0)).siblings(); - } - var tool = $('.l10n-translate', parent); - l10nCommunity.switchPanes(tool, 'translate'); -} - -/** - * Suggestion approval callback. - */ -l10nCommunity.approveSuggestion = function(tid, sid, elem, token) { - // Invoke server side callback to save the approval. - $.ajax({ - type: "GET", - url: Drupal.settings.l10n_approve_callback + tid + Drupal.settings.l10n_form_token_path + token, - success: function (data) { - if (data == 'done') { - // Empty translate pane and inform user that the suggestion was saved. - $('#tpane-'+ sid +' .translate').empty().append(Drupal.settings.l10n_approve_confirm); - // Switch back to translate pane and hide suggestion icon - var parent = $(elem).parents('.translation'); - l10nCommunity.switchPanes($('.l10n-translate', parent), 'translate'); - $('.l10n-suggestions', parent).hide(); - } - else { - alert(Drupal.settings.l10n_approve_error); - }; - }, - error: function (xmlhttp) { - // Being an internal/system error, this is not translatable. - alert('An HTTP error '+ xmlhttp.status +' occured.\n'+ uri); - } - }); - // Return false for onclick handling. - return false; -} - -/** - * Suggestion decline callback. - */ -l10nCommunity.declineSuggestion = function(tid, sid, elem, token) { - // Invoke server side callback to save the decline action. - $.ajax({ - type: "GET", - url: Drupal.settings.l10n_decline_callback + tid + Drupal.settings.l10n_form_token_path + token, - success: function (data) { - if (data == 'done') { - // Reload info pane. - $('#tpane-' + sid + ' .l10n-suggestions').click(); - } - else { - alert(Drupal.settings.l10n_decline_error); - }; - }, - error: function (xmlhttp) { - // Being an internal/system error, this is not translatable. - alert('An HTTP error '+ xmlhttp.status +' occured.\n'+ uri); - } - }); - // Return false for onclick handling. - return false; -} - -/** - * Suggestion editing copy callback. - */ -l10nCommunity.copySuggestion = function(sid, translation) { - if (translation.indexOf("\O") > 0) { - // If we have the null byte, suggestion has plurals, so we need to - // copy over the distinct strings to the distinct textareas. - var strings = translation.split("\O"); - for (string in strings) { - $('#l10n-community-translation-'+ sid +'-'+ string).val(strings[string]); - } - } - else { - // Otherwise standard string. - $('#l10n-community-translation-'+ sid).val(translation); - } - // Show the editing controls. - $('#l10n-community-wrapper-'+ sid).css('display', 'block'); - return false; -} - -// Global killswitch -if (Drupal.jsEnabled) { - $(document).ready(l10nCommunity.init); -} Index: l10n_community/l10n_community.css =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/l10n_server/l10n_community/Attic/l10n_community.css,v retrieving revision 1.1.2.10.2.6 diff -u -r1.1.2.10.2.6 l10n_community.css --- l10n_community/l10n_community.css 10 Sep 2009 09:15:03 -0000 1.1.2.10.2.6 +++ l10n_community/l10n_community.css 13 Dec 2009 10:45:37 -0000 @@ -62,147 +62,3 @@ em.l10n-community-marker { font-weight: bold; } - -/* Translation editor styles */ -#l10n-community-translate-view .pager, -#l10n-community-translate-form .pager { - margin:1em 0em; -} - -table.l10n-server-translate .hidden { - display:none; -} - -table.l10n-server-translate input.form-text, -table.l10n-server-translate textarea { - width:95%; -} - -table.l10n-server-translate td { - vertical-align:top; - width:50%; -} - - table.l10n-server-translate td.source { - padding:10px 5px; - } - - table .string-context { - color: #777; - font-size: 0.8em; - } - - table.l10n-server-translate td.translation { - padding:0px; - } - - table.l10n-server-translate td .l10n-panes { - min-height:60px; - position:relative; - padding:0px 20px 0px 0px; - } - - table.l10n-server-translate td .pane { - padding:10px 5px; - } - - table.l10n-server-translate td .suggestions, - table.l10n-server-translate td .lookup { - display:none; - } - -table.l10n-server-translate .toolbox { - position:absolute; top:0px; right:0px; -} - -table.l10n-server-translate tr .form-item { - white-space: normal; -} - -table.l10n-server-translate label.option { - font-size:90%; -} - -.l10n-button { - cursor:pointer; - display: block; - width: 20px; - height: 20px; - overflow: hidden; - text-indent: -9999px; -} - - .l10n-approval-buttons { - padding-right:10px; - float:right; - } - - .l10n-approval-buttons .l10n-button { - float:left; - } - - .l10n-approval-buttons .l10n-approve, - .l10n-save { - background: url(images/icon_approval.gif) 0px 0px no-repeat; - } - - .l10n-approval-buttons .l10n-approve:active, - .l10n-save:active { - background-position: 0px bottom; - } - - .l10n-approval-buttons .l10n-decline, - .l10n-clear { - width:21px; - background: url(images/icon_approval.gif) -20px 0px no-repeat; - } - - .l10n-approval-buttons .l10n-decline:active, - .l10n-clear:active { - background-position: -20px bottom; - } - - .l10n-community-copy { - background: url(images/icon_copy.gif) no-repeat; - } - - .l10n-translate { - background: url(images/icon_toolbox.gif) 0px 0px no-repeat; - } - - .l10n-translate.active { - background: url(images/icon_toolbox.gif) right 0px no-repeat; - } - - .l10n-suggestions { - background: url(images/icon_toolbox.gif) 0px -40px no-repeat; - } - - .l10n-suggestions.active { - background: url(images/icon_toolbox.gif) right -40px no-repeat; - } - - .l10n-lookup { - background: url(images/icon_toolbox.gif) 0px -20px no-repeat; - } - - .l10n-lookup.active { - background: url(images/icon_toolbox.gif) right -20px no-repeat; - } - -ul.l10n-community-strings { - margin: 0; -} - -ul.l10n-community-strings li { - padding-left: 20px; -} - -ul.l10n-community-strings li .buttons { - margin-left:-20px; - float:left; -} - -.l10n-community-string .original { - display:none; -} Index: l10n_community/l10n_community.info =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/l10n_server/l10n_community/Attic/l10n_community.info,v retrieving revision 1.1.2.4.2.2 diff -u -r1.1.2.4.2.2 l10n_community.info --- l10n_community/l10n_community.info 23 Oct 2008 20:43:36 -0000 1.1.2.4.2.2 +++ l10n_community/l10n_community.info 13 Dec 2009 10:45:37 -0000 @@ -2,6 +2,7 @@ name = "Localization community" description = A community interface for string translation dependencies[] = locale +dependencies[] = jquery_update package = "Localization server" core = 6.x php = 5 Index: l10n_community/l10n_community.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/l10n_server/l10n_community/Attic/l10n_community.module,v retrieving revision 1.1.2.23.2.61 diff -u -r1.1.2.23.2.61 l10n_community.module --- l10n_community/l10n_community.module 5 Dec 2009 13:44:56 -0000 1.1.2.23.2.61 +++ l10n_community/l10n_community.module 13 Dec 2009 10:45:39 -0000 @@ -207,33 +207,10 @@ $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 @@ '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 @@ '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 @@ // 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 @@ } /** + * 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 @@ * @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 @@ * @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); + } } /** @@ -1157,31 +1173,30 @@ * * @see l10n_community_is_duplicate() */ -function l10n_community_target_save($sid, $translation, $langcode, $uid, $suggestion, &$inserted, &$updated, &$unchanged, &$suggested) { +function l10n_community_target_save($sid, $translation, $langcode, $uid, $suggestion) { // Look for an existing active translation, if any. $existing_string = db_fetch_object(db_query("SELECT sid, tid, translation FROM {l10n_community_translation} WHERE sid = %d AND language = '%s' AND is_suggestion = 0 AND is_active = 1", $sid, $langcode)); if (!empty($existing_string->sid)) { - // We have an active translation. if ($existing_string->translation != $translation) { // And what we should save now is different. if ($suggestion) { // Saving a suggestion, so set flag on translation. db_query("UPDATE {l10n_community_translation} SET has_suggestion = 1 WHERE tid = %d", $existing_string->tid); - $suggested++; + l10n_community_counter('suggested'); } else { // Saving a different translation -> deactivate previous translations and suggestions. db_query("UPDATE {l10n_community_translation} SET is_active = 0 WHERE sid = %d AND language = '%s';", $sid, $langcode); - $updated++; + l10n_community_counter('updated'); } db_query("INSERT INTO {l10n_community_translation} (sid, translation, language, uid_entered, time_entered, uid_approved, time_approved, is_suggestion, is_active) VALUES (%d, '%s', '%s', %d, %d, %d, %d, %d, 1)", $sid, $translation, $langcode, $uid, time(), ($suggestion ? 0 : $uid), ($suggestion ? 0 : time()), $suggestion); } else { // Same string as existing translation. - $unchanged++; + l10n_community_counter('unchanged'); } } @@ -1192,12 +1207,12 @@ // suggestions. We track and exclude these by translation = '' later. db_query("INSERT INTO {l10n_community_translation} (sid, translation, language, uid_entered, time_entered, has_suggestion, is_active) VALUES (%d, '', '%s', 0, %d, 1, 1)", $sid, $langcode, time()); db_query("INSERT INTO {l10n_community_translation} (sid, language, translation, uid_entered, time_entered, is_suggestion, is_active) VALUES (%d, '%s', '%s', %d, %d, 1, 1)", $sid, $langcode, $translation, $uid, time()); - $suggested++; + l10n_community_counter('suggested'); } else { // No active translation yet -> INSERT. db_query("INSERT INTO {l10n_community_translation} (sid, translation, language, uid_entered, uid_approved, time_entered, time_approved, is_active) VALUES (%d, '%s', '%s', %d, %d, %d, %d, 1)", $sid, $translation, $langcode, $uid, $uid, time(), time()); - $inserted++; + l10n_community_counter('added'); } } } @@ -1222,34 +1237,68 @@ } /** + * Stores counters for status messages when modifying translations. + * + * @param $field + * The field to increment. Can be one of declined, approved, inserted, + * suggested, duplicates', ignored or unchanged. + * If not specified, the counters are returned and reset afterwards. + * @param $increment + * (Optional) The increment for the counter. Defaults to 1. + */ +function l10n_community_counter($field = NULL, $increment = 1) { + static $counters = array(); + if (isset($field)) { + if (!isset($counters[$field])) { + $counters[$field] = 0; + } + $counters[$field] += $increment; + } + else { + $return = $counters; + $counters = array(); + return $return; + } +} + +/** * Set a message based on the number of translations changed. * * Used by both the save and import process. */ -function l10n_community_update_message($inserted, $updated, $unchanged, $suggested, $duplicates, $ignored) { - // Inform user about changes made. - $message = array(); - if ($inserted) { - $message[] = format_plural($inserted, '1 new translation added', '@count new translations added'); - } - if ($suggested) { - $message[] = format_plural($suggested, '1 new suggestion added', '@count new suggestions added'); - } - if ($updated) { - $message[] = format_plural($updated, '1 translation updated', '@count translations updated'); - } - if ($unchanged) { - $message[] = format_plural($unchanged, '1 translation unchanged', '@count translations unchanged'); - } - if ($duplicates) { - $message[] = format_plural($duplicates, '1 duplicate translation not saved', '@count duplicate translations not saved'); - } - if ($ignored) { - $message[] = format_plural($ignored, '1 source string not found, its translation ignored', '@count source strings not found, their translations were ignored'); - } - if (count($message)) { - drupal_set_message(join('; ', $message) .'.'); - } +function l10n_community_update_message() { + $counters = l10n_community_counter(); + $messages = array(); + + if (!empty($counters['declined'])) + $messages[] = format_plural($counters['declined'], '1 translation declined', '@count translations declined'); + + if (!empty($counters['suggestion_declined'])) + $messages[] = format_plural($counters['suggestion_declined'], '1 suggestion declined', '@count suggestion declined'); + + if (!empty($counters['approved'])) + $messages[] = format_plural($counters['approved'], '1 translation approved', '@count translations approved'); + + if (!empty($counters['added'])) + $messages[] = format_plural($counters['added'], '1 translation added', '@count translations added'); + + if (!empty($counters['suggested'])) + $message[] = format_plural($counters['suggested'], '1 new suggestion added', '@count new suggestions added'); + + if (!empty($counters['updated'])) + $message[] = format_plural($counters['updated'], '1 translation updated', '@count translations updated'); + + if (!empty($counters['duplicates'])) + $message[] = format_plural($counters['duplicates'], '1 duplicate translation not saved', '@count duplicate translations not saved'); + + if (!empty($counters['ignored'])) + $message[] = format_plural($counters['ignored'], '1 source string not found; its translation was ignored', '@count source strings not found; their translations were ignored'); + + if (!empty($counters['unchanged'])) + $message[] = format_plural($counters['unchanged'], '1 translation unchanged', '@count translations unchanged'); + + if ($messages) + drupal_set_message(implode(', ', $messages)); } /** @@ -1310,15 +1359,6 @@ 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_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,11 +1380,23 @@ '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_translate_radio' => array( + 'arguments' => array('element' => NULL), ), - 'l10n_community_in_context' => array( - 'arguments' => array('source' => 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( @@ -1371,77 +1413,6 @@ } /** - * 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) { Index: l10n_community/ajax.inc =================================================================== RCS file: l10n_community/ajax.inc diff -N l10n_community/ajax.inc --- l10n_community/ajax.inc 5 Dec 2009 13:44:56 -0000 1.1.2.18.2.6 +++ /dev/null 1 Jan 1970 00:00:00 -0000 @@ -1,166 +0,0 @@ -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); - $usage_info = ''. t('Used in:') .''. theme('item_list', $output); - - // Information about translator and translation timestamp. - $translation_info = ''; - $translation = db_fetch_object(db_query("SELECT translation, uid_entered, time_entered FROM {l10n_community_translation} WHERE language = '%s' AND sid = %d AND is_active = 1 AND is_suggestion = 0", $langcode, $sid)); - if (!empty($translation->translation)) { - $account = user_load(array('uid' => $translation->uid_entered)); - $translation_info = t('Translated by:
%username at %date', array('%username' => $account->name, '%date' => format_date($translation->time_entered))); - } - - print $usage_info . $translation_info; // . $suggestion_info; - exit; -} - -function l10n_community_string_suggestions($langcode = NULL, $sid = 0) { - global $user; - - // Existing, "unresolved" suggestions. - $suggestions = array(); - $result = db_query("SELECT t.tid, t.sid, t.translation, t.uid_entered, t.time_entered, u.name 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", $langcode, $sid); - - $perm = l10n_community_get_permission($langcode); - while ($suggestion = db_fetch_object($result)) { - // This detail pane is only retrieved from JS, so we are free to output obtrusive JS here. - $token = "'". drupal_get_token('l10n_server_'. $suggestion->tid .'_'. $suggestion->sid) ."'"; - $approve_button = $decline_button = ''; - if ($perm & ($suggestion->uid_entered == $user->uid ? L10N_PERM_MODERATE_OWN : L10N_PERM_MODERATE_OTHERS)) { - $approve_button = theme('l10n_community_button', 'approve', 'l10n-approve', 'onclick="l10nCommunity.approveSuggestion('. $suggestion->tid .','. $suggestion->sid .', this, '. $token .');" title="'. t('Approve suggestion.') .'"'); - $decline_button = theme('l10n_community_button', 'decline', 'l10n-decline', 'onclick="l10nCommunity.declineSuggestion('. $suggestion->tid .','. $suggestion->sid .', this, '. $token .');" title="'. t('Decline suggestion.') .'"'); - } - - // Plural versions are shown in a short form. - // Since we no longer store an additional copy of strings in JS - // and the null byte is stripped out by browsers (?), we need to - // use a DOM-safe delimiter: "; " - $translation = strpos($suggestion->translation, "\0") ? str_replace(chr(0), "; ", $suggestion->translation) : $suggestion->translation; - $translation = l10n_community_format_text($translation, $suggestion->sid); - $suggestions[] = $translation ."
". $approve_button . $decline_button .'
'. t('by %user at %date', array('%user' => (empty($suggestion->name) ? variable_get('anonymous', t('Anonymous')) : $suggestion->name), '%date' => format_date($suggestion->time_entered))) .'
'; - } - if (count($suggestions)) { - // List of suggestions. - $suggestion_info = ''. t('Outstanding suggestions:') .''. theme('l10n_community_strings', $suggestions); - } - else { - $suggestion_info .= ''. t('No suggestions found.') .''; - } - print $suggestion_info; - exit; -} - -/** - * Checks permission of current user to approve or decline $tid. - * - * @param $tid - * Suggestion ID. - * @return - * The suggestion object if validated $tid, user permissions and token. - */ -function l10n_community_string_ajax_suggestion($tid = 0) { - global $user; - - if (($suggestion = db_fetch_object(db_query("SELECT * FROM {l10n_community_translation} WHERE tid = %d AND is_suggestion = 1 AND is_active = 1", $tid))) && - ($perm = l10n_community_get_permission($suggestion->language)) && - ($perm & ($suggestion->uid_entered == $user->uid ? L10N_PERM_MODERATE_OWN : L10N_PERM_MODERATE_OTHERS)) && - !empty($_GET['form_token']) && - drupal_valid_token($_GET['form_token'], 'l10n_server_'. $suggestion->tid .'_'. $suggestion->sid)) { - return $suggestion; - } - return FALSE; -} - -/** - * Records approval of a previous string suggestion. - * - * This callback is invoked from JavaScript. - * - * @param $tid - * Suggestion ID. - */ -function l10n_community_string_approve($tid = 0) { - global $user; - - if ($suggestion = l10n_community_string_ajax_suggestion($tid)) { - // Mark existing translations and suggestions as inactive in this language. - db_query("UPDATE {l10n_community_translation} SET is_active = 0 WHERE sid = %d AND language = '%s'", $suggestion->sid, $suggestion->language); - // 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'", $suggestion->sid, $suggestion->language); - // Mark this exact suggestion as active, and set approval time. - db_query("UPDATE {l10n_community_translation} SET time_approved = %d, uid_approved = %d, has_suggestion = 0, is_suggestion = 0, is_active = 1 WHERE tid = %d;", time(), $user->uid, $suggestion->tid); - // Return something so the client sees we are OK. - print 'done'; - exit; - } - print 'error'; - exit; -} - -/** - * Records decline action of a previous string suggestion. - * - * This callback is invoked from JavaScript. - * - * @param $tid - * Suggestion ID. - */ -function l10n_community_string_decline($tid = 0) { - if ($suggestion = l10n_community_string_ajax_suggestion($tid)) { - // Mark this suggestion as inactive. - db_query("UPDATE {l10n_community_translation} SET is_active = 0 WHERE tid = %d", $tid); - // Let's see if we have any other suggestion 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'", $suggestion->sid, $suggestion->language)); - if (!$count) { - // No remaining suggestions in this language. - db_query("UPDATE {l10n_community_translation} SET has_suggestion = 0 WHERE sid = %d AND is_suggestion = 0 AND is_active = 1 AND language = '%s'", $suggestion->sid, $suggestion->language); - } - // Return something so the client sees we are OK. - print 'done'; - exit; - } - print 'error'; - exit; -} Index: l10n_community/import.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/l10n_server/l10n_community/Attic/import.inc,v retrieving revision 1.1.2.5.2.16 diff -u -r1.1.2.5.2.16 import.inc --- l10n_community/import.inc 5 Dec 2009 13:44:56 -0000 1.1.2.5.2.16 +++ l10n_community/import.inc 13 Dec 2009 10:45:37 -0000 @@ -129,10 +129,8 @@ // Do the actual parsing on the local file. if (l10n_community_import($file, $form_state['values']['langcode'], $form_state['values']['is_suggestion'], $form_state['values']['import_uid'])) { - // Get status report on what was done in the process. - list($inserted, $updated, $unchanged, $suggested, $duplicates, $ignored) = _l10n_community_import_one_string(); drupal_set_message(t('The translation was successfully imported.')); - l10n_community_update_message($inserted, $updated, $unchanged, $suggested, $duplicates, $ignored); + l10n_community_update_message(); cache_clear_all('l10n:stats:'. $form_state['values']['langcode'], 'cache'); } } @@ -331,20 +329,10 @@ * @param $uid * User id used to save attribution information. */ -function _l10n_community_import_one_string($value = NULL, $langcode = NULL, $is_suggestion = FALSE, $uid = NULL) { +function _l10n_community_import_one_string($value, $langcode = NULL, $is_suggestion = FALSE, $uid = NULL) { global $user; - static $inserted = 0; - static $updated = 0; - static $unchanged = 0; - static $suggested = 0; - static $duplicates = 0; - static $ignored = 0; - - if ($value == NULL) { - // Result stats queried. - return array($inserted, $updated, $unchanged, $suggested, $duplicates, $ignored); - } + static $counters = array(); // Trim translation (we will apply source string based whitespace later). if (is_string($value['msgstr'])) { @@ -389,20 +377,20 @@ if ($is_suggestion || !$translation) { if (!l10n_community_is_duplicate($value['msgstr'], $sid, $langcode)) { - l10n_community_target_save($sid, $value['msgstr'], $langcode, $uid, $is_suggestion, $inserted, $updated, $unchanged, $suggested); + l10n_community_target_save($sid, $value['msgstr'], $langcode, $uid, $is_suggestion); } else { - $duplicates++; + l10n_community_counter('duplicates'); } } else { // We certainly did not update this one. - $unchanged++; + l10n_community_counter('unchanged'); } } else { // Source string not found, string ignored. - $ignored++; + l10n_community_counter('ignored'); } } } Index: l10n_community/translate.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/l10n_server/l10n_community/Attic/translate.inc,v retrieving revision 1.1.2.7.2.26 diff -u -r1.1.2.7.2.26 translate.inc --- l10n_community/translate.inc 5 Dec 2009 13:44:56 -0000 1.1.2.7.2.26 +++ l10n_community/translate.inc 13 Dec 2009 10:45:39 -0000 @@ -6,51 +6,6 @@ * 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; -} - // = Filter form handling ====================================================== /** @@ -190,387 +145,558 @@ } } -// = 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 ========================================================== + +/** + * Menu callback: List translations and suggestions + */ +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') .'/jquery13.js'); + drupal_add_js(drupal_get_path('module', 'l10n_community') .'/jquery.worddiff.js'); + drupal_add_js(drupal_get_path('module', 'l10n_community') .'/editor.js'); + + $language = l10n_community_get_language($langcode); + $filters = l10n_community_build_filter_values($_GET); + $strings = l10n_community_get_strings($language->language, $filters, $filters['limit']); + + $output = drupal_get_form('l10n_community_filter_form', $filters); + + 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); + } + + return $output; +} /** - * Translation web interface. + * Form callback: List translations and suggestions. * - * @param $strings - * Array of string objects to display. + * @param $form_state + * The form state array. * @param $language - * Language object. + * A language object. * @param $filters - * Filters used to present this editing view. - * @param $perm - * Community permission level of user watching the page. - */ -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']; - } + * 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']); $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 + '#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), ); foreach ($strings as $string) { - $form[$string->sid] = array( - '#tree' => TRUE, - ); + $form['strings'][$string->sid] = _l10n_community_translate_string($form_state, $string, $language, $permission); + } - // 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' => "
", - ); + return $form; +} - $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); - - $form[$string->sid]['source'] = array( - '#type' => 'item', - '#value' => $source, - ); +/** + * Return a marked-up string. + */ +function _l10n_community_translate_render_string($strings, $empty = '') { + if ($empty) { + $empty = ' data-empty="'. check_plain($empty) .'"'; + } + return "". implode("
", array_map('check_plain', $strings)) .''; +} + +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); + } + + $source->value = l10n_community_unpack_string($source->value); - $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 = array( + '#string' => $source, + '#langcode' => $language->language, + 'source' => array( + 'string' => array('#value' => _l10n_community_translate_render_string($source->value)), + ), + ); + + if ($permission & L10N_PERM_SUGGEST) { + $form['source']['edit'] = array( + '#value' => t('Edit Copy'), + '#prefix' => '', ); + } - if ($is_plural) { + // 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); - // 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), - ); - } + // 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); + } + } - $string_parts = explode(chr(0), $string->value); + // 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); + } - 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 $form; +} - // 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, - ); - } +// Build mock object for new textarea +function _l10n_community_translate_translation_textarea($source, $language) { + global $user; + + 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, + ); +} + +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))), + ); + + 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; + } +} - // Dealing with a simple string (no plurals). +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); + $form = array( + '#theme' => 'l10n_community_translate_translation', + 'original' => array('#type' => 'value', '#value' => $string), + ); + + $form['active'] = array( + '#type' => 'radio', + '#theme' => 'l10n_community_translate_radio', + '#title' => _l10n_community_translate_render_string($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'), + ); + + 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 ($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 ($translated) { - $form[$string->sid]['translation_existing'] = array( - '#type' => 'item', - '#value' => theme('l10n_community_strings', array(l10n_community_format_text($string->translation, $string->sid))), + if ($permission & L10N_PERM_SUGGEST) { + $form['edit'] = array( + '#value' => t('Edit Copy'), + '#prefix' => '', ); } - $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; + if (isset($string->username)) { + $title = l10n_community_translate_byline($string); + + $form['author'] = array( + '#value' => $title, + ); } } + } - // 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', - ); + return $form; +} - if (!($perm & L10N_PERM_MODERATE_OWN)) { - // User with suggestion capability only, record this. - $form[$string->sid]['translation']['is_suggestion'] = array( - '#type' => 'value', - '#value' => TRUE - ); +function theme_l10n_community_translate_actions($element) { + $actions = ''; + foreach (array('declined', /*'stable', */'edit') as $type) { + if (isset($element[$type])) { + $actions .= '
  • '. drupal_render($element[$type]) .'
  • '; } - else { - // User with full privileges, offer option to submit suggestion. - $form[$string->sid]['translation']['is_suggestion'] = array( - '#title' => t('Suggestion for discussion'), - '#type' => 'checkbox', - ); + } + 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']); + } + + return $output . ''; +} + +function theme_l10n_community_translate_radio($element) { + _form_set_class($element, array('form-radio')); + $output = ''; + + if (isset($element['#title'])) { + $output .= ''; + } + + 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]); } } + $output .= '
    '; - // 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' => '✔')), - - '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.'), - - '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' - ); + return $output; +} - // Let the user submit the form. - $form['submit'] = array( - '#type' => 'submit', - '#value' => !($perm & L10N_PERM_MODERATE_OWN) ? t('Save suggestions') : t('Save translations') +function theme_l10n_community_translate_source($element) { + $output = theme('l10n_community_translate_actions', $element['source']); + $output .= ''; + $output .= ''; + return $output; +} + +function theme_l10n_community_translate_table($element) { + $header = array( + t('Source Text'), + t('Translations'), ); - $form['#theme'] = 'l10n_community_translate_form'; + $rows = array(); + 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])), + ); + } - return $form; + return theme('table', $header, $rows, array('class' => 'l10n-table')); } -/** - * Save translations entered in the web form. - */ -function l10n_community_translate_form_submit($form, &$form_state) { - global $user; - $inserted = $updated = $unchanged = $suggested = $duplicates = $ignored = 0; +function l10n_community_translate_submit($form, &$form_state) { + $langcode = $form_state['values']['langcode']; - 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; - } - - $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 (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 = ''; + 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 (!empty($text)) { - // Check for duplicate translation or suggestion. - if (l10n_community_is_duplicate($text, $sid, $form_state['values']['langcode'])) { - $duplicates++; - continue; + 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); + } } - - // 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 - ); } } - // Inform user about changes made to the database. - l10n_community_update_message($inserted, $updated, $unchanged, $suggested, $duplicates, $ignored); + l10n_community_update_message(); } -// = Theme functions =========================================================== - /** - * Theme function for l10n_community_filter_form. + * 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 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); - } +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); } - // 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')); } /** - * Theme function for l10n_community_translate_form. + * 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 theme_l10n_community_translate_form($form) { - $rows = array(); - $output = ''; +function l10n_community_add_suggestion($langcode, $sid, $translation) { + global $user; - 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]); - } + // 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; } - $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; + 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); +} + +// = Miscellaneous ============================================================= + +/** * Theme context information for source strings. * * @param $string @@ -615,10 +741,12 @@ $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'])) { @@ -659,7 +787,7 @@ // 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 @@ -670,7 +798,7 @@ 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 @@ -755,6 +883,51 @@ return $filter; } +// = 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; +} + /** * Replace complex data filters (objects or arrays) with string representations. * Index: l10n_remote/l10n_remote.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/l10n_server/l10n_remote/Attic/l10n_remote.module,v retrieving revision 1.1.2.4 diff -u -r1.1.2.4 l10n_remote.module --- l10n_remote/l10n_remote.module 5 Dec 2009 13:44:56 -0000 1.1.2.4 +++ l10n_remote/l10n_remote.module 13 Dec 2009 10:45:39 -0000 @@ -141,7 +141,7 @@ //watchdog('l10n_community', 'Language not allowed for remote submission.', NULL, WATCHDOG_WARNING); return array('status' => FALSE, 'reason' => 'Language not accepted.'); } - + // Check if the user has permission to submit strings in this language. if (!(l10n_community_get_permission($langcode, $account) & L10N_PERM_SUGGEST)) { //watchdog('l10n_community', 'Not allowed to submit translations in this language remotely.', NULL, WATCHDOG_WARNING); Index: l10n_community/editor.css =================================================================== RCS file: l10n_community/editor.css diff -N l10n_community/editor.css --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ l10n_community/editor.css 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,298 @@ +body { + font-family:Verdana; + font-size:12px; +} + + +#l10n-community-list-form .sticky-header { + z-index:2; +} + +.l10n-table .form-item { + margin:0; + padding:0; +} + +.l10n-table td { + vertical-align:top; +} + +.l10n-table ul { margin:0; padding:0; } + +.l10n-table li { + list-style: none; + background: none; + padding:0; + margin:0; +} + +td.source { + width:45%; +} + td.source .l10n-string { + padding:4px 6px; + } + td.source .l10n-usage { + padding:0 6px 6px 16px; + font-size:10px; + line-height:12px; + } + td.source .l10n-usage .l10n-more { + font-size:10px; + text-decoration:none; + color:#000; + opacity:0.4; + } + td.source .l10n-usage .l10n-more:before { + content:"▾ "; + } + td.source .l10n-usage .l10n-more:hover, + td.source .l10n-usage .l10n-more:active { + color:#666; + } + td.source .l10n-usage li { + list-style-type:disc; + background:none; + margin:0 0 0 22px; + padding:0; + } + +li.translation { + padding:4px 0; + padding-left:20px; + clear:left; + position:relative; + -webkit-transition: opacity 0.2s ease-out; +} + +li.translation > .selector { + position:absolute; + left:2px; + top:5px; + margin:0; +} + + +.l10n-table ul.actions { + float:right; +} + li.translation:not(.is-active) > .actions li.stable, + li.translation.is-active > .actions li.declined { display:none; } + .l10n-table ul.actions { + overflow:hidden; + float: right; + margin-right:4px; + -webkit-user-select:none; + } + .l10n-table ul.actions li { + float:right; + font-size:80%; + line-height:20px; + padding:0 0 4px 4px; + } + .l10n-table ul.actions li label { + width:16px; + height:16px; + overflow:hidden; + display:block; + text-indent:-1000px; + opacity:0.6; + cursor:pointer; + } + .l10n-table ul.actions li label:active { + position:relative; top:1px; + } + .l10n-table ul.actions li.declined label { + background:url(images/decline.png) no-repeat right top; + } + .l10n-table ul.actions li.edit label { + background:url(images/edit.png) no-repeat right top; + } + li.translation.is-declined > .actions li.declined label { + background-image:url(images/undecline.png); + } + .l10n-table ul.actions li.stable label { + background:url(images/stable-inactive.png) no-repeat right top; + } + li.translation.is-stable > .actions li.stable label, + li.translation:hover > .actions li label { + opacity:1; + } + li.translation.is-stable > .actions li.stable label { + background-image:url(images/stable-active.png); + } + + .l10n-string > br { + display:block; + content:""; + height:1px; + background:#CCC; + margin:2px 0; + } + +li.translation .l10n-string, +td.source .l10n-string { + display:block; + line-height:16px; +} + li.translation .l10n-string > span, + td.source .l10n-string > span { + white-space:pre-wrap; + -webkit-transition: opacity 0.2s ease-out; + } + li.translation.is-declined .l10n-string { + color:#999; + height:16px; + overflow:hidden; + text-decoration:line-through; + } + li.translation.no-translation .l10n-string span { + font-style:italic; + color:#666; + } + +li.translation > .author { + font-size:10px; + padding-left:10px; + line-height:12px; + color:#999; +} + li.translation.is-declined > .author { + display:none; + } + li.translation > .author a { + color:#666; + } + li.translation > .author span[title] { + cursor:pointer; + } + + + li.new-translation:not(.focussed):not(.has-content):not(.is-active) > .l10n-string { + display:none; + } + li.new-translation.has-content > .actions { + display:block; + } + li.new-translation .l10n-string { + display:block; + margin-bottom:4px; + margin-right:6px; + } + li.new-translation .l10n-string > span:empty:before { + content:attr(data-empty); + font-style:italic; + color:#666; + } + li.new-translation > .form-item ~ .form-item { + margin-top:5px; + } + li.new-translation:not(.has-content):not(.focussed) > .form-item ~ .form-item { + display:none; + } + li.new-translation textarea { + height:60px; + clear:left; + width:100%; + padding:0; + margin:0; + font-size:100%; + font-family:inherit; + -webkit-transition: height 0.2s ease-out; + } + li.new-translation:not(.focussed) textarea:not(:focus):hover { + opacity:0.75; + } + li.new-translation:not(.focussed) .grippie { + visibility:hidden; + margin-bottom:-9px; + } + li.new-translation textarea:not(:focus) { + height:19px !important; + overflow:hidden; + resize:none; + } + li.new-translation:not(.focussed) textarea { + opacity:0.5 !important; + } + li.new-translation.focussed textarea { + opacity:1; + position:relative; + z-index:1; + } + + + + + +span.l10n-escape, +span.l10n-nl::after, +em.l10n-placeholder, +code + { + font-family:Menlo, Monaco, Consolas, "Lucida Console", monospace; + font-size:12px; + line-height:14px; +} + + + + +.l10n-string span.l10n-nl::after { + content:"¬"; + opacity:0.4; + line-height:19px; + vertical-align:top; +} + + +.l10n-string code, +.l10n-string em.l10n-placeholder { + background:transparent; + padding:0 1px; + margin:0 -1px; + -webkit-border-radius:4px; + -moz-border-radius:4px; + border:1px solid transparent; + font-style:normal; +} + +tr:hover .l10n-string code, +tr:hover .l10n-string em.l10n-placeholder { + background:rgba(0,0,0,0.05); +} + +tr:hover .l10n-string em.l10n-placeholder { +} + +tr:hover .l10n-string em.l10n-placeholder.highlight { +border-color:rgba(0,0,0,0.2); + background:rgba(0,0,0,0.1); +} + +.l10n-string code em.l10n-placeholder { + top:0; +} + +.l10n-string span.worddiff-del del, +.l10n-string span.worddiff-ins ins { + text-decoration: none; + border:1px solid transparent; + -webkit-border-radius:4px; + -moz-border-radius:4px; + padding:0; + margin:0 -1px; +} + +.l10n-string > span.worddiff-del { background: #FFD8D8; } +.l10n-string > span.worddiff-del del { background: #FF8888; } +.l10n-string > span.worddiff-ins { background: #DDF8CC; } +.l10n-string > span.worddiff-ins ins { background: #90F678; } + +.clearfix:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; + } \ No newline at end of file Index: l10n_community/editor.js =================================================================== RCS file: l10n_community/editor.js diff -N l10n_community/editor.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ l10n_community/editor.js 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,202 @@ + +(function ($) { + encode = function (str) { + str = String(str); + var replace = { '&': '&', '<': '<', '>': '>' }; + for (var character in replace) { + var regex = new RegExp(character, 'g'); + str = str.replace(regex, replace[character]); + } + return str; + }; + + $('em.l10n-placeholder') + .live('mouseover', function () { + $(this).closest('tr').find('.l10n-placeholder:contains("' + $(this).text() + '")').addClass('highlight'); + }) + .live('mouseout', function () { + $('.l10n-placeholder.highlight').removeClass('highlight'); + }); + + $(function () { + $('.l10n-more').click(function () { + $(this) + .addClass('loading') + .parent().load(this.href); + return false; + }); + + var markup = function (string) { + // Highlight placeholders. + string = string.replace(/([!@%]|<(ins|del)>[!@%]<\/(ins|del)>)(\w+|<(ins|del)>\w+<\/(ins|del)>)/g, '$&'); + + // Wrap HTML tags in tags. + string = string.replace(/(<.+?(>|$))/g, function (str) { + return '' + str.replace(/<[^>]+>/g, '$&') + ''; + }); + + string = string.replace(/\\[^<]/g, '$&'); + string = string.replace(/\n/g, '$&'); + return string; + }; + + $('td.translation').parent().each(function () { + var all = $('li.translation', this); + var strings = all.find('.l10n-string > span'); + var source = $('td.source', this); + + source.find('.l10n-string span').each(function () { + $(this).html(markup($(this).html())); + }); + + strings.each(function () { + var orig = $(this).html(), markedUp = markup(orig); + $(this) + .html(markedUp) + .data('worddiff:original', orig) + .data('worddiff:markup', markedUp); + }); + + var setStatus = function (elem, status, value) { + newValue = elem.find('.' + status + ' :checkbox').attr('checked', value).attr('checked'); + elem[(newValue === undefined ? value : newValue) ? 'addClass' : 'removeClass']('is-' + status); + }; + + var textareas = all.filter('.new-translation').find('textarea'); + + $(this).find('ul.actions .edit').click(function () { + var translation = $(this).closest('td.source, li.translation'); + var confirmed = undefined; + textareas.each(function (i) { + var textarea = $(this); + var val = textarea.val(); + if (confirmed || val === textarea.attr('defaultValue') || !val || (confirmed === undefined && (confirmed = confirm("Do you want to overwrite the current suggestion?")))) { + textarea.val(translation.find('.l10n-string > span:eq('+ i +')').text()).keyup(); + if (i == 0) { + // Since we can't have multiple focuses, we jut focus the first textarea. + textarea.focus(); + } + } + }); + }); + + all.each(function () { + var translation = $(this); + var isTranslation = !translation.is('.no-translation'); + var siblings = all.not(this).not('.no-translation'); + + var removeDiff = function () { + strings.worddiffRevert(); + }; + + var updateDiff = function () { + removeDiff(); + if (isTranslation) { + var orig = siblings.filter('.is-active'); + if (!orig.length) + orig = siblings.filter('.default'); + if (!orig.length) + orig = all.not('.no-translation').eq(0).not(translation); + if (orig.length) { + orig = orig.find('.l10n-string > span'); + translation.find('.l10n-string > span').each(function (i) { + $(this).worddiff(orig.get(i), markup); + }); + } + } + }; + + translation.find('> .selector').click(function () { + setStatus(translation, 'declined', false); + // Mark this as the active translation. + setStatus(translation.siblings('.is-active:not(.new-translation)'), 'declined', true); + setStatus(translation.siblings('.is-active'), 'active', false); + translation.addClass('is-active'); + }); + + translation.find('> .actions .declined :checkbox').change(function () { + setStatus(translation, 'declined', this.checked); + }); + + translation.find('> .actions .stable :checkbox').change(function () { + setStatus(translation, 'stable', this.checked); + }); + + translation.find('> .author span[title]').click(function () { + var $this = $(this), html = $this.html(); + $this.html($this.attr('title')).attr('title', html); + }); + + if (isTranslation) { + translation.find('.l10n-string').dblclick(function () { + translation.siblings().not('.new-translation').each(function () { + setStatus($(this), 'declined', true); + }); + }); + + translation + .mouseenter(updateDiff) + .mouseleave(removeDiff); + } + + if (translation.is('.new-translation')) { + translation.find('> .selector').click(function () { + textareas.each(function () { + var textarea = $(this); + if (textarea.val() === '' || textarea.val() === textarea.attr('defaultValue')) { + textarea.focus(); + // Stop checking the other ones. + return false; + } + }); + }); + + var hasContent = function () { + for (var i = 0; i < textareas.length; i++) { + if (textareas[i].value && textareas[i].value !== textareas[i].defaultValue) { + return true; + } + } + return false; + }; + + var blurTimeout; + textareas.each(function (n) { + var wrapper = $(this); + var textarea = $(this); + var text = translation.find('.l10n-string > span').eq(n); + + textarea + .focus(function () { + translation.addClass('focussed'); + clearTimeout(blurTimeout); + if (textarea.val() === textarea.attr('defaultValue')) { + textarea.val(''); + } + }) + .blur(function () { + blurTimeout = setTimeout(function () { + translation.removeClass('focussed'); + if (textarea.val() === '') { + textarea.val(textarea.attr('defaultValue')); + } + translation[hasContent() ? 'addClass' : 'removeClass']('has-content'); + }, 1000); + }) + .keyup(function () { + var val = encode(textarea.val()); + text + .data('worddiff:original', val) + .data('worddiff:markup', markup(val)); + var oldPos = textarea.offset().top; + updateDiff(); + var diff = textarea.offset().top - oldPos; + if (diff) + window.scrollBy(0, diff); + }); + }); + } + }); + }); + }); +})(jQuery); Index: l10n_community/jquery.worddiff.js =================================================================== RCS file: l10n_community/jquery.worddiff.js diff -N l10n_community/jquery.worddiff.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ l10n_community/jquery.worddiff.js 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,183 @@ +(function($) { + var Fragment = function (string) { + this.content = string; + this.equiv = false; + }; + + Fragment.prototype.toString = function (tag) { + if (this.equiv || !tag) + return this.content; + else + return '<' + tag + '>' + this.content + ''; + }; + + + var moveToEnd = function (a, i, k) { + if (!a.equiv && (!k[i-1] || k[i-1].equiv)) { + // Find next item equiv item. + for (var j = i+1; k[j] && !k[j].equiv; j++); + if (k[j] && k[j].content === a.content) { + k[i] = k[j]; + k[j] = a; + } + } + }; + + var aggregate = function (a, i, k) { + if (!a.equiv && k[i+1] && !k[i+1].equiv) { + k[i+1].content = a.content + k[i+1].content; + delete k[i]; + } + }; + + var join = function (what, t) { + return $.map(what, function (a) { + if (a) return a.toString(t); + }).join(''); + }; + + + $.wordDiff = { + nonWord: /(&.+?;|[\u0000-\u0040\u005B-\u0060\u007B-\u00A9\u00AB-\u00B4\u00B6-\u00B9\u00BB-\u00BF\u00D7\u00F7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u036F\u0375\u037E\u0384\u0385\u0387\u03F6\u0482-\u0489\u055A-\u055F\u0589\u058A\u0591-\u05C7\u05F3\u05F4\u0600-\u0603\u0606-\u061B\u061E\u061F\u064B-\u065E\u0660-\u066D\u0670\u06D4\u06D6-\u06E4\u06EA-\u06ED\u06F0-\u06F9\u06FD\u06FE\u0700-\u070D\u070F\u0711\u0730-\u074A\u07A6-\u07B0\u07C0-\u07C9\u07EB-\u07F3\u07F6-\u07F9\u0901-\u0903\u093C\u093E-\u094D\u0951-\u0954\u09E2\u0962-\u0970\u06E7-\u06E9\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E3\u09E6-\u09EF\u09F2-\u09FA\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A66-\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AE6-\u0AEF\u0AF1\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B66-\u0B70\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0BE6-\u0BFA\u0C01-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C66-\u0C6F\u0C78-\u0C7F\u0C82\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D02\u0D03\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D66-\u0D75\u0D79\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2-\u0DF4\u0E31\u0E34-\u0E3A\u0E3F\u0E47-\u0E5B\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0ED0-\u0ED9\u0F01-\u0F3F\u0F71-\u0F87\u0F90-\u0F97\u0F99-\u0FBC\u0FBE-\u0FCC\u0FCE-\u0FD4\u102B-\u103E\u1040-\u104F\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F-\u1099\u109E\u109F\u10FB\u135F-\u137C\u1390-\u1399\u166D\u166E\u1680\u169B\u169C\u16EB-\u16F0\u1712-\u1714\u1732-\u1736\u1752\u1753\u1772\u1773\u17B4-\u17D6\u17D8-\u17DB\u17DD\u17E0-\u17E9\u17F0-\u17F9\u1800-\u180E\u1810-\u1819\u18A9\u1920-\u192B\u1930-\u193B\u1940\u1944-\u194F\u19B0-\u19C0\u19C8\u19C9\u19D0-\u19D9\u19DE-\u19FF\u1A17-\u1A1B\u1A1E\u1A1F\u1B00-\u1B04\u1B34-\u1B44\u1B50-\u1B7C\u1B80-\u1B82\u1BA1-\u1BAA\u1BB0-\u1BB9\u1C24-\u1C37\u1C3B-\u1C49\u1C50-\u1C59\u1C7E\u1C7F\u1DC0-\u1DE6\u1DFE\u1DFF\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u2000-\u2064\u206A-\u2070\u2074-\u207E\u2080-\u208E\u20A0-\u20B5\u20D0-\u20F0\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F\u2153-\u2182\u2185-\u2188\u2190-\u23E7\u2400-\u2426\u2440-\u244A\u2460-\u269D\u26A0-\u26BC\u26C0-\u26C3\u2701-\u2704\u2706-\u2709\u270C-\u2727\u2729-\u274B\u274D\u274F-\u2752\u2756\u2758-\u275E\u2761-\u2794\u2798-\u27AF\u27B1-\u27BE\u27C0-\u27CA\u27CC\u27D0-\u2B4C\u2B50-\u2B54\u2CE5-\u2CEA\u2CF9-\u2CFF\u2DE0-\u2E2E\u2E30\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3000-\u3004\u3007-\u3030\u3036-\u303A\u303D-\u303F\u3099-\u309C\u30A0\u30FB\u3190-\u319F\u31C0-\u31E3\u3200-\u321E\u3220-\u3243\u3250-\u32FE\u3300-\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA60D-\uA60F\uA620-\uA629\uA66F-\uA673\uA67C-\uA67E\uA700-\uA716\uA720\uA721\uA789\uA78A\uA802\uA806\uA80B\uA823-\uA82B\uA874-\uA877\uA880\uA881\uA8B4-\uA8C4\uA8CE-\uA8D9\uA900-\uA909\uA926-\uA92F\uA947-\uA953\uA95F\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA50-\uAA59\uAA5C-\uAA5F\uD800\uDB7F\uDB80\uDBFF\uDC00\uDFFF\uE000\uF8FF\uFB1E\uFB29\uFD3E\uFD3F\uFDFC\uFDFD\uFE00-\uFE19\uFE20-\uFE26\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFEFF\uFF01-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFF9-\uFFFD])/, + + tokenize: function (args) { + // Split on non-word characters. + for (var type in args) { + args[type] = $.grep( + args[type].split($.wordDiff.nonWord), + function (s) { return s.length; } + ); + } + + // Calculate the indexes and offsets for common suffixes and prefixes. + var i = -1, j = args.del.length, k = args.ins.length; + while (args.del[++i] === args.ins[i] && i <= j); + while (j >= i && k >= i && args.del[--j] === args.ins[--k]); + + args.prefix = args.del.slice(0, i).join(''); + args.suffix = args.del.slice(j + 1).join(''); + args.del = args.del.slice(i, ++j); + args.ins = args.ins.slice(i, ++k); + }, + + lcs: function (args) { + var matrix = []; + + for (var i = 0; i < args.del.length; i++) { + matrix[i] = []; + for (var j = 0; j < args.ins.length; j++) { + if (args.del[i] === args.ins[j]) { + matrix[i][j] = (matrix[i - 1] && matrix[i - 1][j - 1] || 0) + args.del[i].length; + } + else { + matrix[i][j] = Math.max(matrix[i][j - 1] || 0, matrix[i - 1] && matrix[i - 1][j] || 0); + } + } + } + + return matrix; + }, + + changeset: function (args, matrix) { + var result = {}; + + $.each(['del', 'ins'], function () { + result[this] = $.map(args[this], function (a) { return new Fragment(a); }); + }); + + // Backtrack through the matrix. + for (var i = result.del.length - 1, j = result.ins.length - 1; i >= 0; i--, j--) { + if (j < 0 || result.del[i].content !== result.ins[j].content) { + if (j < 0 || (j > 0 && matrix[i - 1] && (matrix[i][j - 1] < matrix[i - 1][j]))) { + j++; + } + else { + i++; + } + } + else { + result.del[i] = result.ins[j]; + result.del[i].equiv = true; + } + } + + // Fill up gaps. + for (var i = 0; i < result.del.length; i++) { + if (result.del[i].equiv && result.del[i].content.length < 3) { + var j = result.ins.indexOf(result.del[i]); + if (result.del[i-1] && result.del[i+1] && result.ins[j-1] && result.ins[j+1] && !result.del[i-1].equiv && !result.del[i+1].equiv && !result.ins[j-1].equiv && !result.ins[j+1].equiv){ + result.del[i].equiv = false; + result.ins[j] = $.extend(true, {}, result.del[i]); + } + } + } + + $.each(['del', 'ins'], function () { + // Try to move changes to the end. + for (var i = 0; i < result[this].length; i++) + moveToEnd(result[this][i], i, result[this]); + + // Aggregate subsequent changes to minimize ins/del tags. + for (var i = 0; i < result[this].length; i++) + aggregate(result[this][i], i, result[this]); + }); + + return result; + }, + + render: function (args, result) { + var diff = { + del: args.prefix + join(result.del, 'del') + args.suffix, + ins: args.prefix + join(result.ins, 'ins') + args.suffix + }; + + return diff; + }, + + diff: function (del, ins) { + var args = { 'del': del, 'ins': ins }; + + $.wordDiff.tokenize(args); + var matrix = $.wordDiff.lcs(args); + var result = $.wordDiff.changeset(args, matrix); + return $.wordDiff.render(args, result); + } + }; + + $.fn.worddiff = function (selector, processor) { + var src = $(selector), del = src.data('worddiff:original'); + if (del === undefined) + src.data('worddiff:original', del = src.html()); + + return this.each(function () { + var dst = $(this), ins = dst.data('worddiff:original'); + if (ins === undefined) + dst.data('worddiff:original', ins = dst.html()); + + var diff = $.wordDiff.diff(del, ins); + if (processor) { + diff.del = processor(diff.del); + diff.ins = processor(diff.ins); + } + + if (diff.del && diff.ins) { + src.html(diff.del).addClass('worddiff-del'); + dst.html(diff.ins).addClass('worddiff-ins'); + } + }); + }; + + $.fn.worddiffRevert = function () { + return this.each(function () { + var dest = $(this), orig = dest.data('worddiff:markup'); + if (orig === undefined) { + orig = dest.data('worddiff:original'); + } + if (orig !== undefined) { + dest + .html(orig) + .removeClass('worddiff-del').removeClass('worddiff-ins'); + } + }); + }; +})(jQuery); \ No newline at end of file