diff --git l10n_community/ajax.inc l10n_community/ajax.inc deleted file mode 100644 index 5fe7fcd..0000000 --- l10n_community/ajax.inc +++ /dev/null @@ -1,158 +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) { - // 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 = $perm == L10N_PERM_ALL ? theme('l10n_community_button', 'approve', 'l10n-approve', 'onclick="l10nCommunity.approveSuggestion('. $suggestion->tid .','. $suggestion->sid .', this, '. $token .');" title="'. t('Approve suggestion.') .'"') : ''; - $decline_button = $perm == L10N_PERM_ALL ? 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) { - if (($suggestion = db_fetch_object(db_query("SELECT * FROM {l10n_community_translation} WHERE tid = %d AND is_suggestion = 1 AND is_active = 1", $tid))) && - (l10n_community_get_permission($suggestion->language) == L10N_PERM_ALL) && - !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; -} diff --git l10n_community/editor.css l10n_community/editor.css new file mode 100644 index 0000000..9faa2ef --- /dev/null +++ l10n_community/editor.css @@ -0,0 +1,296 @@ +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:#CCC; + } + 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; +} + + +li.translation > .actions { + float:right; +} + li.translation:not(.is-active) > .actions li.stable, + li.translation.is-active > .actions li.declined { display:none; } + li.translation > .actions { + overflow:hidden; + float: right; + margin-right:4px; + -webkit-user-select:none; + } + li.translation > .actions li { + float:right; + font-size:80%; + line-height:20px; + padding:0 0 4px 4px; + } + li.translation > .actions li label { + width:16px; + height:16px; + overflow:hidden; + display:block; + text-indent:-1000px; + opacity:0.6; + } + li.translation > .actions li label:active { + position:relative; top:1px; + } + li.translation > .actions li.declined label { + background:url(images/decline.png) no-repeat right top; + } + li.translation > .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); + } + li.translation > .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 diff --git l10n_community/editor.js l10n_community/editor.js new file mode 100644 index 0000000..96f3d27 --- /dev/null +++ l10n_community/editor.js @@ -0,0 +1,205 @@ + +(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.source').each(function () { + var source = $(this); + + source.find('.l10n-string span').each(function () { + $(this).html(markup($(this).html())); + }); + }); + + $('td.translation').each(function () { + var all = $('li.translation', this); + var strings = all.find('.l10n-string > span'); + + 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'); + + 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); + }); + + translation.find('> .actions .edit').click(function () { + 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(); + } + } + }); + + }); + + 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); + }); + }); + } + }); + }); + }); +})(jQ13); diff --git l10n_community/images/decline.png l10n_community/images/decline.png new file mode 100755 index 0000000..c8a5058 Binary files /dev/null and l10n_community/images/decline.png differ diff --git l10n_community/images/edit.png l10n_community/images/edit.png new file mode 100755 index 0000000..37aced3 Binary files /dev/null and l10n_community/images/edit.png differ diff --git l10n_community/images/icon_approval.gif l10n_community/images/icon_approval.gif deleted file mode 100644 index b4290ba..0000000 Binary files l10n_community/images/icon_approval.gif and /dev/null differ diff --git l10n_community/images/icon_copy.gif l10n_community/images/icon_copy.gif deleted file mode 100644 index 4be8507..0000000 Binary files l10n_community/images/icon_copy.gif and /dev/null differ diff --git l10n_community/images/icon_toolbox.gif l10n_community/images/icon_toolbox.gif deleted file mode 100644 index 3c3f40e..0000000 Binary files l10n_community/images/icon_toolbox.gif and /dev/null differ diff --git l10n_community/images/newline.png l10n_community/images/newline.png deleted file mode 100644 index 5b7c3dc..0000000 Binary files l10n_community/images/newline.png and /dev/null differ diff --git l10n_community/images/stable-active.png l10n_community/images/stable-active.png new file mode 100755 index 0000000..53542e7 Binary files /dev/null and l10n_community/images/stable-active.png differ diff --git l10n_community/images/stable-inactive.png l10n_community/images/stable-inactive.png new file mode 100755 index 0000000..262915a Binary files /dev/null and l10n_community/images/stable-inactive.png differ diff --git l10n_community/images/undecline.png l10n_community/images/undecline.png new file mode 100755 index 0000000..35ad7c2 Binary files /dev/null and l10n_community/images/undecline.png differ diff --git l10n_community/import.inc l10n_community/import.inc index d28e2bb..cc23f7a 100644 --- l10n_community/import.inc +++ l10n_community/import.inc @@ -11,7 +11,8 @@ */ function l10n_community_import_page($langcode) { $languages = l10n_community_get_languages(); - if (($perm = l10n_community_get_permission($langcode)) == L10N_PERM_ALL) { + $permission = l10n_community_get_permission($langcode); + if ($permission & L10N_PERMISSION_MODERATE_OWN) { drupal_set_title(t('Import translations to @language', array('@language' => $languages[$langcode]->name))); } else { @@ -23,13 +24,13 @@ function l10n_community_import_page($langcode) { l(t('Home'), NULL), l(t('Translate projects'), 'translate')) ); - return drupal_get_form('l10n_community_import_form', $langcode, $perm); + return drupal_get_form('l10n_community_import_form', $langcode, $permission); } /** * Translation import form. */ -function l10n_community_import_form($form_state, $langcode, $perm) { +function l10n_community_import_form($form_state, $langcode, $permission) { $form['#attributes']['enctype'] = 'multipart/form-data'; $form['langcode'] = array( @@ -49,7 +50,7 @@ function l10n_community_import_form($form_state, $langcode, $perm) { '#description' => t('Check if the translations you import need discussion. If checked, all translations in the imported file will be saved as suggestions.'), ); // Restrict to suggestions if no permission to submit translations directly. - if ($perm != L10N_PERM_ALL) { + if ($permission & L10N_PERMISSION_MODERATE_OWN) { $form['is_suggestion']['#type'] = 'value'; $form['is_suggestion']['#value'] = 1; } @@ -99,7 +100,7 @@ function l10n_community_import_form_submit($form, &$form_state) { // Get status report on what was done in the process. list($inserted, $updated, $unchanged, $suggested) = _l10n_community_import_one_string(); drupal_set_message(t('The translation was successfully imported.')); - l10n_community_update_message($inserted, $updated, $unchanged, $suggested, 0); + l10n_community_update_message(0, 0, $inserted, $updated, $unchanged, $suggested); cache_clear_all('l10n:stats:'. $form_state['values']['langcode'], 'cache'); } } diff --git l10n_community/jquery.worddiff.js l10n_community/jquery.worddiff.js new file mode 100644 index 0000000..30d7464 --- /dev/null +++ l10n_community/jquery.worddiff.js @@ -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'); + } + }); + }; +})(jQ13); \ No newline at end of file diff --git l10n_community/jquery13.js l10n_community/jquery13.js new file mode 100644 index 0000000..e8a7732 --- /dev/null +++ l10n_community/jquery13.js @@ -0,0 +1,23 @@ +// $Id: jquery.js,v 1.17 2009-06-30 03:35:11 webchick Exp $ +/*jsl:ignore*/ +/* + * jQuery JavaScript Library v1.3.2 + * http://jquery.com/ + * + * Copyright (c) 2009 John Resig + * Dual licensed under the MIT and GPL licenses. + * http://docs.jquery.com/License + * + * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009) + * Revision: 6246 + */ +(function(){var l=this,g,y=l.jQuery,p=l.$,o=l.jQuery=l.$=function(E,F){return new o.fn.init(E,F)},D=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,f=/^.[^:#\[\.,]*$/;o.fn=o.prototype={init:function(E,H){E=E||document;if(E.nodeType){this[0]=E;this.length=1;this.context=E;return this}if(typeof E==="string"){var G=D.exec(E);if(G&&(G[1]||!H)){if(G[1]){E=o.clean([G[1]],H)}else{var I=document.getElementById(G[3]);if(I&&I.id!=G[3]){return o().find(E)}var F=o(I||[]);F.context=document;F.selector=E;return F}}else{return o(H).find(E)}}else{if(o.isFunction(E)){return o(document).ready(E)}}if(E.selector&&E.context){this.selector=E.selector;this.context=E.context}return this.setArray(o.isArray(E)?E:o.makeArray(E))},selector:"",jquery:"1.3.2",size:function(){return this.length},get:function(E){return E===g?Array.prototype.slice.call(this):this[E]},pushStack:function(F,H,E){var G=o(F);G.prevObject=this;G.context=this.context;if(H==="find"){G.selector=this.selector+(this.selector?" ":"")+E}else{if(H){G.selector=this.selector+"."+H+"("+E+")"}}return G},setArray:function(E){this.length=0;Array.prototype.push.apply(this,E);return this},each:function(F,E){return o.each(this,F,E)},index:function(E){return o.inArray(E&&E.jquery?E[0]:E,this)},attr:function(F,H,G){var E=F;if(typeof F==="string"){if(H===g){return this[0]&&o[G||"attr"](this[0],F)}else{E={};E[F]=H}}return this.each(function(I){for(F in E){o.attr(G?this.style:this,F,o.prop(this,E[F],G,I,F))}})},css:function(E,F){if((E=="width"||E=="height")&&parseFloat(F)<0){F=g}return this.attr(E,F,"curCSS")},text:function(F){if(typeof F!=="object"&&F!=null){return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(F))}var E="";o.each(F||this,function(){o.each(this.childNodes,function(){if(this.nodeType!=8){E+=this.nodeType!=1?this.nodeValue:o.fn.text([this])}})});return E},wrapAll:function(E){if(this[0]){var F=o(E,this[0].ownerDocument).clone();if(this[0].parentNode){F.insertBefore(this[0])}F.map(function(){var G=this;while(G.firstChild){G=G.firstChild}return G}).append(this)}return this},wrapInner:function(E){return this.each(function(){o(this).contents().wrapAll(E)})},wrap:function(E){return this.each(function(){o(this).wrapAll(E)})},append:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.appendChild(E)}})},prepend:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.insertBefore(E,this.firstChild)}})},before:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this)})},after:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this.nextSibling)})},end:function(){return this.prevObject||o([])},push:[].push,sort:[].sort,splice:[].splice,find:function(E){if(this.length===1){var F=this.pushStack([],"find",E);F.length=0;o.find(E,this[0],F);return F}else{return this.pushStack(o.unique(o.map(this,function(G){return o.find(E,G)})),"find",E)}},clone:function(G){var E=this.map(function(){if(!o.support.noCloneEvent&&!o.isXMLDoc(this)){var I=this.outerHTML;if(!I){var J=this.ownerDocument.createElement("div");J.appendChild(this.cloneNode(true));I=J.innerHTML}return o.clean([I.replace(/ jQuery\d+="(?:\d+|null)"/g,"").replace(/^\s*/,"")])[0]}else{return this.cloneNode(true)}});if(G===true){var H=this.find("*").andSelf(),F=0;E.find("*").andSelf().each(function(){if(this.nodeName!==H[F].nodeName){return}var I=o.data(H[F],"events");for(var K in I){for(var J in I[K]){o.event.add(this,K,I[K][J],I[K][J].data)}}F++})}return E},filter:function(E){return this.pushStack(o.isFunction(E)&&o.grep(this,function(G,F){return E.call(G,F)})||o.multiFilter(E,o.grep(this,function(F){return F.nodeType===1})),"filter",E)},closest:function(E){var G=o.expr.match.POS.test(E)?o(E):null,F=0;return this.map(function(){var H=this;while(H&&H.ownerDocument){if(G?G.index(H)>-1:o(H).is(E)){o.data(H,"closest",F);return H}H=H.parentNode;F++}})},not:function(E){if(typeof E==="string"){if(f.test(E)){return this.pushStack(o.multiFilter(E,this,true),"not",E)}else{E=o.multiFilter(E,this)}}var F=E.length&&E[E.length-1]!==g&&!E.nodeType;return this.filter(function(){return F?o.inArray(this,E)<0:this!=E})},add:function(E){return this.pushStack(o.unique(o.merge(this.get(),typeof E==="string"?o(E):o.makeArray(E))))},is:function(E){return !!E&&o.multiFilter(E,this).length>0},hasClass:function(E){return !!E&&this.is("."+E)},val:function(K){if(K===g){var E=this[0];if(E){if(o.nodeName(E,"option")){return(E.attributes.value||{}).specified?E.value:E.text}if(o.nodeName(E,"select")){var I=E.selectedIndex,L=[],M=E.options,H=E.type=="select-one";if(I<0){return null}for(var F=H?I:0,J=H?I+1:M.length;F=0||o.inArray(this.name,K)>=0)}else{if(o.nodeName(this,"select")){var N=o.makeArray(K);o("option",this).each(function(){this.selected=(o.inArray(this.value,N)>=0||o.inArray(this.text,N)>=0)});if(!N.length){this.selectedIndex=-1}}else{this.value=K}}})},html:function(E){return E===g?(this[0]?this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g,""):null):this.empty().append(E)},replaceWith:function(E){return this.after(E).remove()},eq:function(E){return this.slice(E,+E+1)},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments),"slice",Array.prototype.slice.call(arguments).join(","))},map:function(E){return this.pushStack(o.map(this,function(G,F){return E.call(G,F,G)}))},andSelf:function(){return this.add(this.prevObject)},domManip:function(J,M,L){if(this[0]){var I=(this[0].ownerDocument||this[0]).createDocumentFragment(),F=o.clean(J,(this[0].ownerDocument||this[0]),I),H=I.firstChild;if(H){for(var G=0,E=this.length;G1||G>0?I.cloneNode(true):I)}}if(F){o.each(F,z)}}return this;function K(N,O){return M&&o.nodeName(N,"table")&&o.nodeName(O,"tr")?(N.getElementsByTagName("tbody")[0]||N.appendChild(N.ownerDocument.createElement("tbody"))):N}}};o.fn.init.prototype=o.fn;function z(E,F){if(F.src){o.ajax({url:F.src,async:false,dataType:"script"})}else{o.globalEval(F.text||F.textContent||F.innerHTML||"")}if(F.parentNode){F.parentNode.removeChild(F)}}function e(){return +new Date}o.extend=o.fn.extend=function(){var J=arguments[0]||{},H=1,I=arguments.length,E=false,G;if(typeof J==="boolean"){E=J;J=arguments[1]||{};H=2}if(typeof J!=="object"&&!o.isFunction(J)){J={}}if(I==H){J=this;--H}for(;H-1}},swap:function(H,G,I){var E={};for(var F in G){E[F]=H.style[F];H.style[F]=G[F]}I.call(H);for(var F in G){H.style[F]=E[F]}},css:function(H,F,J,E){if(F=="width"||F=="height"){var L,G={position:"absolute",visibility:"hidden",display:"block"},K=F=="width"?["Left","Right"]:["Top","Bottom"];function I(){L=F=="width"?H.offsetWidth:H.offsetHeight;if(E==="border"){return}o.each(K,function(){if(!E){L-=parseFloat(o.curCSS(H,"padding"+this,true))||0}if(E==="margin"){L+=parseFloat(o.curCSS(H,"margin"+this,true))||0}else{L-=parseFloat(o.curCSS(H,"border"+this+"Width",true))||0}})}if(H.offsetWidth!==0){I()}else{o.swap(H,G,I)}return Math.max(0,Math.round(L))}return o.curCSS(H,F,J)},curCSS:function(I,F,G){var L,E=I.style;if(F=="opacity"&&!o.support.opacity){L=o.attr(E,"opacity");return L==""?"1":L}if(F.match(/float/i)){F=w}if(!G&&E&&E[F]){L=E[F]}else{if(q.getComputedStyle){if(F.match(/float/i)){F="float"}F=F.replace(/([A-Z])/g,"-$1").toLowerCase();var M=q.getComputedStyle(I,null);if(M){L=M.getPropertyValue(F)}if(F=="opacity"&&L==""){L="1"}}else{if(I.currentStyle){var J=F.replace(/\-(\w)/g,function(N,O){return O.toUpperCase()});L=I.currentStyle[F]||I.currentStyle[J];if(!/^\d+(px)?$/i.test(L)&&/^\d/.test(L)){var H=E.left,K=I.runtimeStyle.left;I.runtimeStyle.left=I.currentStyle.left;E.left=L||0;L=E.pixelLeft+"px";E.left=H;I.runtimeStyle.left=K}}}}return L},clean:function(F,K,I){K=K||document;if(typeof K.createElement==="undefined"){K=K.ownerDocument||K[0]&&K[0].ownerDocument||document}if(!I&&F.length===1&&typeof F[0]==="string"){var H=/^<(\w+)\s*\/?>$/.exec(F[0]);if(H){return[K.createElement(H[1])]}}var G=[],E=[],L=K.createElement("div");o.each(F,function(P,S){if(typeof S==="number"){S+=""}if(!S){return}if(typeof S==="string"){S=S.replace(/(<(\w+)[^>]*?)\/>/g,function(U,V,T){return T.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?U:V+">"});var O=S.replace(/^\s+/,"").substring(0,10).toLowerCase();var Q=!O.indexOf("",""]||!O.indexOf("",""]||O.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"","
"]||!O.indexOf("",""]||(!O.indexOf("",""]||!O.indexOf("",""]||!o.support.htmlSerialize&&[1,"div
","
"]||[0,"",""];L.innerHTML=Q[1]+S+Q[2];while(Q[0]--){L=L.lastChild}if(!o.support.tbody){var R=/"&&!R?L.childNodes:[];for(var M=N.length-1;M>=0;--M){if(o.nodeName(N[M],"tbody")&&!N[M].childNodes.length){N[M].parentNode.removeChild(N[M])}}}if(!o.support.leadingWhitespace&&/^\s/.test(S)){L.insertBefore(K.createTextNode(S.match(/^\s*/)[0]),L.firstChild)}S=o.makeArray(L.childNodes)}if(S.nodeType){G.push(S)}else{G=o.merge(G,S)}});if(I){for(var J=0;G[J];J++){if(o.nodeName(G[J],"script")&&(!G[J].type||G[J].type.toLowerCase()==="text/javascript")){E.push(G[J].parentNode?G[J].parentNode.removeChild(G[J]):G[J])}else{if(G[J].nodeType===1){G.splice.apply(G,[J+1,0].concat(o.makeArray(G[J].getElementsByTagName("script"))))}I.appendChild(G[J])}}return E}return G},attr:function(J,G,K){if(!J||J.nodeType==3||J.nodeType==8){return g}var H=!o.isXMLDoc(J),L=K!==g;G=H&&o.props[G]||G;if(J.tagName){var F=/href|src|style/.test(G);if(G=="selected"&&J.parentNode){J.parentNode.selectedIndex}if(G in J&&H&&!F){if(L){if(G=="type"&&o.nodeName(J,"input")&&J.parentNode){throw"type property can't be changed"}J[G]=K}if(o.nodeName(J,"form")&&J.getAttributeNode(G)){return J.getAttributeNode(G).nodeValue}if(G=="tabIndex"){var I=J.getAttributeNode("tabIndex");return I&&I.specified?I.value:J.nodeName.match(/(button|input|object|select|textarea)/i)?0:J.nodeName.match(/^(a|area)$/i)&&J.href?0:g}return J[G]}if(!o.support.style&&H&&G=="style"){return o.attr(J.style,"cssText",K)}if(L){J.setAttribute(G,""+K)}var E=!o.support.hrefNormalized&&H&&F?J.getAttribute(G,2):J.getAttribute(G);return E===null?g:E}if(!o.support.opacity&&G=="opacity"){if(L){J.zoom=1;J.filter=(J.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(K)+""=="NaN"?"":"alpha(opacity="+K*100+")")}return J.filter&&J.filter.indexOf("opacity=")>=0?(parseFloat(J.filter.match(/opacity=([^)]*)/)[1])/100)+"":""}G=G.replace(/-([a-z])/ig,function(M,N){return N.toUpperCase()});if(L){J[G]=K}return J[G]},trim:function(E){return(E||"").replace(/^\s+|\s+$/g,"")},makeArray:function(G){var E=[];if(G!=null){var F=G.length;if(F==null||typeof G==="string"||o.isFunction(G)||G.setInterval){E[0]=G}else{while(F){E[--F]=G[F]}}}return E},inArray:function(G,H){for(var E=0,F=H.length;E0?this.clone(true):this).get();o.fn[F].apply(o(L[K]),I);J=J.concat(I)}return this.pushStack(J,E,G)}});o.each({removeAttr:function(E){o.attr(this,E,"");if(this.nodeType==1){this.removeAttribute(E)}},addClass:function(E){o.className.add(this,E)},removeClass:function(E){o.className.remove(this,E)},toggleClass:function(F,E){if(typeof E!=="boolean"){E=!o.className.has(this,F)}o.className[E?"add":"remove"](this,F)},remove:function(E){if(!E||o.filter(E,[this]).length){o("*",this).add([this]).each(function(){o.event.remove(this);o.removeData(this)});if(this.parentNode){this.parentNode.removeChild(this)}}},empty:function(){o(this).children().remove();while(this.firstChild){this.removeChild(this.firstChild)}}},function(E,F){o.fn[E]=function(){return this.each(F,arguments)}});function j(E,F){return E[0]&&parseInt(o.curCSS(E[0],F,true),10)||0}var h="jQuery"+e(),v=0,A={};o.extend({cache:{},data:function(F,E,G){F=F==l?A:F;var H=F[h];if(!H){H=F[h]=++v}if(E&&!o.cache[H]){o.cache[H]={}}if(G!==g){o.cache[H][E]=G}return E?o.cache[H][E]:H},removeData:function(F,E){F=F==l?A:F;var H=F[h];if(E){if(o.cache[H]){delete o.cache[H][E];E="";for(E in o.cache[H]){break}if(!E){o.removeData(F)}}}else{try{delete F[h]}catch(G){if(F.removeAttribute){F.removeAttribute(h)}}delete o.cache[H]}},queue:function(F,E,H){if(F){E=(E||"fx")+"queue";var G=o.data(F,E);if(!G||o.isArray(H)){G=o.data(F,E,o.makeArray(H))}else{if(H){G.push(H)}}}return G},dequeue:function(H,G){var E=o.queue(H,G),F=E.shift();if(!G||G==="fx"){F=E[0]}if(F!==g){F.call(H)}}});o.fn.extend({data:function(E,G){var H=E.split(".");H[1]=H[1]?"."+H[1]:"";if(G===g){var F=this.triggerHandler("getData"+H[1]+"!",[H[0]]);if(F===g&&this.length){F=o.data(this[0],E)}return F===g&&H[1]?this.data(H[0]):F}else{return this.trigger("setData"+H[1]+"!",[H[0],G]).each(function(){o.data(this,E,G)})}},removeData:function(E){return this.each(function(){o.removeData(this,E)})},queue:function(E,F){if(typeof E!=="string"){F=E;E="fx"}if(F===g){return o.queue(this[0],E)}return this.each(function(){var G=o.queue(this,E,F);if(E=="fx"&&G.length==1){G[0].call(this)}})},dequeue:function(E){return this.each(function(){o.dequeue(this,E)})}}); +/* + * Sizzle CSS Selector Engine - v0.9.3 + * Copyright 2009, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){var R=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?/g,L=0,H=Object.prototype.toString;var F=function(Y,U,ab,ac){ab=ab||[];U=U||document;if(U.nodeType!==1&&U.nodeType!==9){return[]}if(!Y||typeof Y!=="string"){return ab}var Z=[],W,af,ai,T,ad,V,X=true;R.lastIndex=0;while((W=R.exec(Y))!==null){Z.push(W[1]);if(W[2]){V=RegExp.rightContext;break}}if(Z.length>1&&M.exec(Y)){if(Z.length===2&&I.relative[Z[0]]){af=J(Z[0]+Z[1],U)}else{af=I.relative[Z[0]]?[U]:F(Z.shift(),U);while(Z.length){Y=Z.shift();if(I.relative[Y]){Y+=Z.shift()}af=J(Y,af)}}}else{var ae=ac?{expr:Z.pop(),set:E(ac)}:F.find(Z.pop(),Z.length===1&&U.parentNode?U.parentNode:U,Q(U));af=F.filter(ae.expr,ae.set);if(Z.length>0){ai=E(af)}else{X=false}while(Z.length){var ah=Z.pop(),ag=ah;if(!I.relative[ah]){ah=""}else{ag=Z.pop()}if(ag==null){ag=U}I.relative[ah](ai,ag,Q(U))}}if(!ai){ai=af}if(!ai){throw"Syntax error, unrecognized expression: "+(ah||Y)}if(H.call(ai)==="[object Array]"){if(!X){ab.push.apply(ab,ai)}else{if(U.nodeType===1){for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&(ai[aa]===true||ai[aa].nodeType===1&&K(U,ai[aa]))){ab.push(af[aa])}}}else{for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&ai[aa].nodeType===1){ab.push(af[aa])}}}}}else{E(ai,ab)}if(V){F(V,U,ab,ac);if(G){hasDuplicate=false;ab.sort(G);if(hasDuplicate){for(var aa=1;aa":function(Z,U,aa){var X=typeof U==="string";if(X&&!/\W/.test(U)){U=aa?U:U.toUpperCase();for(var V=0,T=Z.length;V=0)){if(!V){T.push(Y)}}else{if(V){U[X]=false}}}}return false},ID:function(T){return T[1].replace(/\\/g,"")},TAG:function(U,T){for(var V=0;T[V]===false;V++){}return T[V]&&Q(T[V])?U[1]:U[1].toUpperCase()},CHILD:function(T){if(T[1]=="nth"){var U=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(T[2]=="even"&&"2n"||T[2]=="odd"&&"2n+1"||!/\D/.test(T[2])&&"0n+"+T[2]||T[2]);T[2]=(U[1]+(U[2]||1))-0;T[3]=U[3]-0}T[0]=L++;return T},ATTR:function(X,U,V,T,Y,Z){var W=X[1].replace(/\\/g,"");if(!Z&&I.attrMap[W]){X[1]=I.attrMap[W]}if(X[2]==="~="){X[4]=" "+X[4]+" "}return X},PSEUDO:function(X,U,V,T,Y){if(X[1]==="not"){if(X[3].match(R).length>1||/^\w/.test(X[3])){X[3]=F(X[3],null,null,U)}else{var W=F.filter(X[3],U,V,true^Y);if(!V){T.push.apply(T,W)}return false}}else{if(I.match.POS.test(X[0])||I.match.CHILD.test(X[0])){return true}}return X},POS:function(T){T.unshift(true);return T}},filters:{enabled:function(T){return T.disabled===false&&T.type!=="hidden"},disabled:function(T){return T.disabled===true},checked:function(T){return T.checked===true},selected:function(T){T.parentNode.selectedIndex;return T.selected===true},parent:function(T){return !!T.firstChild},empty:function(T){return !T.firstChild},has:function(V,U,T){return !!F(T[3],V).length},header:function(T){return/h\d/i.test(T.nodeName)},text:function(T){return"text"===T.type},radio:function(T){return"radio"===T.type},checkbox:function(T){return"checkbox"===T.type},file:function(T){return"file"===T.type},password:function(T){return"password"===T.type},submit:function(T){return"submit"===T.type},image:function(T){return"image"===T.type},reset:function(T){return"reset"===T.type},button:function(T){return"button"===T.type||T.nodeName.toUpperCase()==="BUTTON"},input:function(T){return/input|select|textarea|button/i.test(T.nodeName)}},setFilters:{first:function(U,T){return T===0},last:function(V,U,T,W){return U===W.length-1},even:function(U,T){return T%2===0},odd:function(U,T){return T%2===1},lt:function(V,U,T){return UT[3]-0},nth:function(V,U,T){return T[3]-0==U},eq:function(V,U,T){return T[3]-0==U}},filter:{PSEUDO:function(Z,V,W,aa){var U=V[1],X=I.filters[U];if(X){return X(Z,W,V,aa)}else{if(U==="contains"){return(Z.textContent||Z.innerText||"").indexOf(V[3])>=0}else{if(U==="not"){var Y=V[3];for(var W=0,T=Y.length;W=0)}}},ID:function(U,T){return U.nodeType===1&&U.getAttribute("id")===T},TAG:function(U,T){return(T==="*"&&U.nodeType===1)||U.nodeName===T},CLASS:function(U,T){return(" "+(U.className||U.getAttribute("class"))+" ").indexOf(T)>-1},ATTR:function(Y,W){var V=W[1],T=I.attrHandle[V]?I.attrHandle[V](Y):Y[V]!=null?Y[V]:Y.getAttribute(V),Z=T+"",X=W[2],U=W[4];return T==null?X==="!=":X==="="?Z===U:X==="*="?Z.indexOf(U)>=0:X==="~="?(" "+Z+" ").indexOf(U)>=0:!U?Z&&T!==false:X==="!="?Z!=U:X==="^="?Z.indexOf(U)===0:X==="$="?Z.substr(Z.length-U.length)===U:X==="|="?Z===U||Z.substr(0,U.length+1)===U+"-":false},POS:function(X,U,V,Y){var T=U[2],W=I.setFilters[T];if(W){return W(X,V,U,Y)}}}};var M=I.match.POS;for(var O in I.match){I.match[O]=RegExp(I.match[O].source+/(?![^\[]*\])(?![^\(]*\))/.source)}var E=function(U,T){U=Array.prototype.slice.call(U);if(T){T.push.apply(T,U);return T}return U};try{Array.prototype.slice.call(document.documentElement.childNodes)}catch(N){E=function(X,W){var U=W||[];if(H.call(X)==="[object Array]"){Array.prototype.push.apply(U,X)}else{if(typeof X.length==="number"){for(var V=0,T=X.length;V";var T=document.documentElement;T.insertBefore(U,T.firstChild);if(!!document.getElementById(V)){I.find.ID=function(X,Y,Z){if(typeof Y.getElementById!=="undefined"&&!Z){var W=Y.getElementById(X[1]);return W?W.id===X[1]||typeof W.getAttributeNode!=="undefined"&&W.getAttributeNode("id").nodeValue===X[1]?[W]:g:[]}};I.filter.ID=function(Y,W){var X=typeof Y.getAttributeNode!=="undefined"&&Y.getAttributeNode("id");return Y.nodeType===1&&X&&X.nodeValue===W}}T.removeChild(U)})();(function(){var T=document.createElement("div");T.appendChild(document.createComment(""));if(T.getElementsByTagName("*").length>0){I.find.TAG=function(U,Y){var X=Y.getElementsByTagName(U[1]);if(U[1]==="*"){var W=[];for(var V=0;X[V];V++){if(X[V].nodeType===1){W.push(X[V])}}X=W}return X}}T.innerHTML="";if(T.firstChild&&typeof T.firstChild.getAttribute!=="undefined"&&T.firstChild.getAttribute("href")!=="#"){I.attrHandle.href=function(U){return U.getAttribute("href",2)}}})();if(document.querySelectorAll){(function(){var T=F,U=document.createElement("div");U.innerHTML="

";if(U.querySelectorAll&&U.querySelectorAll(".TEST").length===0){return}F=function(Y,X,V,W){X=X||document;if(!W&&X.nodeType===9&&!Q(X)){try{return E(X.querySelectorAll(Y),V)}catch(Z){}}return T(Y,X,V,W)};F.find=T.find;F.filter=T.filter;F.selectors=T.selectors;F.matches=T.matches})()}if(document.getElementsByClassName&&document.documentElement.getElementsByClassName){(function(){var T=document.createElement("div");T.innerHTML="
";if(T.getElementsByClassName("e").length===0){return}T.lastChild.className="e";if(T.getElementsByClassName("e").length===1){return}I.order.splice(1,0,"CLASS");I.find.CLASS=function(U,V,W){if(typeof V.getElementsByClassName!=="undefined"&&!W){return V.getElementsByClassName(U[1])}}})()}function P(U,Z,Y,ad,aa,ac){var ab=U=="previousSibling"&&!ac;for(var W=0,V=ad.length;W0){X=T;break}}}T=T[U]}ad[W]=X}}}var K=document.compareDocumentPosition?function(U,T){return U.compareDocumentPosition(T)&16}:function(U,T){return U!==T&&(U.contains?U.contains(T):true)};var Q=function(T){return T.nodeType===9&&T.documentElement.nodeName!=="HTML"||!!T.ownerDocument&&Q(T.ownerDocument)};var J=function(T,aa){var W=[],X="",Y,V=aa.nodeType?[aa]:aa;while((Y=I.match.PSEUDO.exec(T))){X+=Y[0];T=T.replace(I.match.PSEUDO,"")}T=I.relative[T]?T+"*":T;for(var Z=0,U=V.length;Z0||T.offsetHeight>0};F.selectors.filters.animated=function(T){return o.grep(o.timers,function(U){return T===U.elem}).length};o.multiFilter=function(V,T,U){if(U){V=":not("+V+")"}return F.matches(V,T)};o.dir=function(V,U){var T=[],W=V[U];while(W&&W!=document){if(W.nodeType==1){T.push(W)}W=W[U]}return T};o.nth=function(X,T,V,W){T=T||1;var U=0;for(;X;X=X[V]){if(X.nodeType==1&&++U==T){break}}return X};o.sibling=function(V,U){var T=[];for(;V;V=V.nextSibling){if(V.nodeType==1&&V!=U){T.push(V)}}return T};return;l.Sizzle=F})();o.event={add:function(I,F,H,K){if(I.nodeType==3||I.nodeType==8){return}if(I.setInterval&&I!=l){I=l}if(!H.guid){H.guid=this.guid++}if(K!==g){var G=H;H=this.proxy(G);H.data=K}var E=o.data(I,"events")||o.data(I,"events",{}),J=o.data(I,"handle")||o.data(I,"handle",function(){return typeof o!=="undefined"&&!o.event.triggered?o.event.handle.apply(arguments.callee.elem,arguments):g});J.elem=I;o.each(F.split(/\s+/),function(M,N){var O=N.split(".");N=O.shift();H.type=O.slice().sort().join(".");var L=E[N];if(o.event.specialAll[N]){o.event.specialAll[N].setup.call(I,K,O)}if(!L){L=E[N]={};if(!o.event.special[N]||o.event.special[N].setup.call(I,K,O)===false){if(I.addEventListener){I.addEventListener(N,J,false)}else{if(I.attachEvent){I.attachEvent("on"+N,J)}}}}L[H.guid]=H;o.event.global[N]=true});I=null},guid:1,global:{},remove:function(K,H,J){if(K.nodeType==3||K.nodeType==8){return}var G=o.data(K,"events"),F,E;if(G){if(H===g||(typeof H==="string"&&H.charAt(0)==".")){for(var I in G){this.remove(K,I+(H||""))}}else{if(H.type){J=H.handler;H=H.type}o.each(H.split(/\s+/),function(M,O){var Q=O.split(".");O=Q.shift();var N=RegExp("(^|\\.)"+Q.slice().sort().join(".*\\.")+"(\\.|$)");if(G[O]){if(J){delete G[O][J.guid]}else{for(var P in G[O]){if(N.test(G[O][P].type)){delete G[O][P]}}}if(o.event.specialAll[O]){o.event.specialAll[O].teardown.call(K,Q)}for(F in G[O]){break}if(!F){if(!o.event.special[O]||o.event.special[O].teardown.call(K,Q)===false){if(K.removeEventListener){K.removeEventListener(O,o.data(K,"handle"),false)}else{if(K.detachEvent){K.detachEvent("on"+O,o.data(K,"handle"))}}}F=null;delete G[O]}}})}for(F in G){break}if(!F){var L=o.data(K,"handle");if(L){L.elem=null}o.removeData(K,"events");o.removeData(K,"handle")}}},trigger:function(I,K,H,E){var G=I.type||I;if(!E){I=typeof I==="object"?I[h]?I:o.extend(o.Event(G),I):o.Event(G);if(G.indexOf("!")>=0){I.type=G=G.slice(0,-1);I.exclusive=true}if(!H){I.stopPropagation();if(this.global[G]){o.each(o.cache,function(){if(this.events&&this.events[G]){o.event.trigger(I,K,this.handle.elem)}})}}if(!H||H.nodeType==3||H.nodeType==8){return g}I.result=g;I.target=H;K=o.makeArray(K);K.unshift(I)}I.currentTarget=H;var J=o.data(H,"handle");if(J){J.apply(H,K)}if((!H[G]||(o.nodeName(H,"a")&&G=="click"))&&H["on"+G]&&H["on"+G].apply(H,K)===false){I.result=false}if(!E&&H[G]&&!I.isDefaultPrevented()&&!(o.nodeName(H,"a")&&G=="click")){this.triggered=true;try{H[G]()}catch(L){}}this.triggered=false;if(!I.isPropagationStopped()){var F=H.parentNode||H.ownerDocument;if(F){o.event.trigger(I,K,F,true)}}},handle:function(K){var J,E;K=arguments[0]=o.event.fix(K||l.event);K.currentTarget=this;var L=K.type.split(".");K.type=L.shift();J=!L.length&&!K.exclusive;var I=RegExp("(^|\\.)"+L.slice().sort().join(".*\\.")+"(\\.|$)");E=(o.data(this,"events")||{})[K.type];for(var G in E){var H=E[G];if(J||I.test(H.type)){K.handler=H;K.data=H.data;var F=H.apply(this,arguments);if(F!==g){K.result=F;if(F===false){K.preventDefault();K.stopPropagation()}}if(K.isImmediatePropagationStopped()){break}}}},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(H){if(H[h]){return H}var F=H;H=o.Event(F);for(var G=this.props.length,J;G;){J=this.props[--G];H[J]=F[J]}if(!H.target){H.target=H.srcElement||document}if(H.target.nodeType==3){H.target=H.target.parentNode}if(!H.relatedTarget&&H.fromElement){H.relatedTarget=H.fromElement==H.target?H.toElement:H.fromElement}if(H.pageX==null&&H.clientX!=null){var I=document.documentElement,E=document.body;H.pageX=H.clientX+(I&&I.scrollLeft||E&&E.scrollLeft||0)-(I.clientLeft||0);H.pageY=H.clientY+(I&&I.scrollTop||E&&E.scrollTop||0)-(I.clientTop||0)}if(!H.which&&((H.charCode||H.charCode===0)?H.charCode:H.keyCode)){H.which=H.charCode||H.keyCode}if(!H.metaKey&&H.ctrlKey){H.metaKey=H.ctrlKey}if(!H.which&&H.button){H.which=(H.button&1?1:(H.button&2?3:(H.button&4?2:0)))}return H},proxy:function(F,E){E=E||function(){return F.apply(this,arguments)};E.guid=F.guid=F.guid||E.guid||this.guid++;return E},special:{ready:{setup:B,teardown:function(){}}},specialAll:{live:{setup:function(E,F){o.event.add(this,F[0],c)},teardown:function(G){if(G.length){var E=0,F=RegExp("(^|\\.)"+G[0]+"(\\.|$)");o.each((o.data(this,"events").live||{}),function(){if(F.test(this.type)){E++}});if(E<1){o.event.remove(this,G[0],c)}}}}}};o.Event=function(E){if(!this.preventDefault){return new o.Event(E)}if(E&&E.type){this.originalEvent=E;this.type=E.type}else{this.type=E}this.timeStamp=e();this[h]=true};function k(){return false}function u(){return true}o.Event.prototype={preventDefault:function(){this.isDefaultPrevented=u;var E=this.originalEvent;if(!E){return}if(E.preventDefault){E.preventDefault()}E.returnValue=false},stopPropagation:function(){this.isPropagationStopped=u;var E=this.originalEvent;if(!E){return}if(E.stopPropagation){E.stopPropagation()}E.cancelBubble=true},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=u;this.stopPropagation()},isDefaultPrevented:k,isPropagationStopped:k,isImmediatePropagationStopped:k};var a=function(F){var E=F.relatedTarget;while(E&&E!=this){try{E=E.parentNode}catch(G){E=this}}if(E!=this){F.type=F.data;o.event.handle.apply(this,arguments)}};o.each({mouseover:"mouseenter",mouseout:"mouseleave"},function(F,E){o.event.special[E]={setup:function(){o.event.add(this,F,a,E)},teardown:function(){o.event.remove(this,F,a)}}});o.fn.extend({bind:function(F,G,E){return F=="unload"?this.one(F,G,E):this.each(function(){o.event.add(this,F,E||G,E&&G)})},one:function(G,H,F){var E=o.event.proxy(F||H,function(I){o(this).unbind(I,E);return(F||H).apply(this,arguments)});return this.each(function(){o.event.add(this,G,E,F&&H)})},unbind:function(F,E){return this.each(function(){o.event.remove(this,F,E)})},trigger:function(E,F){return this.each(function(){o.event.trigger(E,F,this)})},triggerHandler:function(E,G){if(this[0]){var F=o.Event(E);F.preventDefault();F.stopPropagation();o.event.trigger(F,G,this[0]);return F.result}},toggle:function(G){var E=arguments,F=1;while(F=0){var E=G.slice(I,G.length);G=G.slice(0,I)}var H="GET";if(J){if(o.isFunction(J)){K=J;J=null}else{if(typeof J==="object"){J=o.param(J);H="POST"}}}var F=this;o.ajax({url:G,type:H,dataType:"html",data:J,complete:function(M,L){if(L=="success"||L=="notmodified"){F.html(E?o("
").append(M.responseText.replace(//g,"")).find(E):M.responseText)}if(K){F.each(K,[M.responseText,L,M])}}});return this},serialize:function(){return o.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?o.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password|search/i.test(this.type))}).map(function(E,F){var G=o(this).val();return G==null?null:o.isArray(G)?o.map(G,function(I,H){return{name:F.name,value:I}}):{name:F.name,value:G}}).get()}});o.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(E,F){o.fn[F]=function(G){return this.bind(F,G)}});var r=e();o.extend({get:function(E,G,H,F){if(o.isFunction(G)){H=G;G=null}return o.ajax({type:"GET",url:E,data:G,success:H,dataType:F})},getScript:function(E,F){return o.get(E,null,F,"script")},getJSON:function(E,F,G){return o.get(E,F,G,"json")},post:function(E,G,H,F){if(o.isFunction(G)){H=G;G={}}return o.ajax({type:"POST",url:E,data:G,success:H,dataType:F})},ajaxSetup:function(E){o.extend(o.ajaxSettings,E)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return l.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest()},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(M){M=o.extend(true,M,o.extend(true,{},o.ajaxSettings,M));var W,F=/=\?(&|$)/g,R,V,G=M.type.toUpperCase();if(M.data&&M.processData&&typeof M.data!=="string"){M.data=o.param(M.data)}if(M.dataType=="jsonp"){if(G=="GET"){if(!M.url.match(F)){M.url+=(M.url.match(/\?/)?"&":"?")+(M.jsonp||"callback")+"=?"}}else{if(!M.data||!M.data.match(F)){M.data=(M.data?M.data+"&":"")+(M.jsonp||"callback")+"=?"}}M.dataType="json"}if(M.dataType=="json"&&(M.data&&M.data.match(F)||M.url.match(F))){W="jsonp"+r++;if(M.data){M.data=(M.data+"").replace(F,"="+W+"$1")}M.url=M.url.replace(F,"="+W+"$1");M.dataType="script";l[W]=function(X){V=X;I();L();l[W]=g;try{delete l[W]}catch(Y){}if(H){H.removeChild(T)}}}if(M.dataType=="script"&&M.cache==null){M.cache=false}if(M.cache===false&&G=="GET"){var E=e();var U=M.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+E+"$2");M.url=U+((U==M.url)?(M.url.match(/\?/)?"&":"?")+"_="+E:"")}if(M.data&&G=="GET"){M.url+=(M.url.match(/\?/)?"&":"?")+M.data;M.data=null}if(M.global&&!o.active++){o.event.trigger("ajaxStart")}var Q=/^(\w+:)?\/\/([^\/?#]+)/.exec(M.url);if(M.dataType=="script"&&G=="GET"&&Q&&(Q[1]&&Q[1]!=location.protocol||Q[2]!=location.host)){var H=document.getElementsByTagName("head")[0];var T=document.createElement("script");T.src=M.url;if(M.scriptCharset){T.charset=M.scriptCharset}if(!W){var O=false;T.onload=T.onreadystatechange=function(){if(!O&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){O=true;I();L();T.onload=T.onreadystatechange=null;H.removeChild(T)}}}H.appendChild(T);return g}var K=false;var J=M.xhr();if(M.username){J.open(G,M.url,M.async,M.username,M.password)}else{J.open(G,M.url,M.async)}try{if(M.data){J.setRequestHeader("Content-Type",M.contentType)}if(M.ifModified){J.setRequestHeader("If-Modified-Since",o.lastModified[M.url]||"Thu, 01 Jan 1970 00:00:00 GMT")}J.setRequestHeader("X-Requested-With","XMLHttpRequest");J.setRequestHeader("Accept",M.dataType&&M.accepts[M.dataType]?M.accepts[M.dataType]+", */*":M.accepts._default)}catch(S){}if(M.beforeSend&&M.beforeSend(J,M)===false){if(M.global&&!--o.active){o.event.trigger("ajaxStop")}J.abort();return false}if(M.global){o.event.trigger("ajaxSend",[J,M])}var N=function(X){if(J.readyState==0){if(P){clearInterval(P);P=null;if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}}else{if(!K&&J&&(J.readyState==4||X=="timeout")){K=true;if(P){clearInterval(P);P=null}R=X=="timeout"?"timeout":!o.httpSuccess(J)?"error":M.ifModified&&o.httpNotModified(J,M.url)?"notmodified":"success";if(R=="success"){try{V=o.httpData(J,M.dataType,M)}catch(Z){R="parsererror"}}if(R=="success"){var Y;try{Y=J.getResponseHeader("Last-Modified")}catch(Z){}if(M.ifModified&&Y){o.lastModified[M.url]=Y}if(!W){I()}}else{o.handleError(M,J,R)}L();if(X){J.abort()}if(M.async){J=null}}}};if(M.async){var P=setInterval(N,13);if(M.timeout>0){setTimeout(function(){if(J&&!K){N("timeout")}},M.timeout)}}try{J.send(M.data)}catch(S){o.handleError(M,J,null,S)}if(!M.async){N()}function I(){if(M.success){M.success(V,R)}if(M.global){o.event.trigger("ajaxSuccess",[J,M])}}function L(){if(M.complete){M.complete(J,R)}if(M.global){o.event.trigger("ajaxComplete",[J,M])}if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}return J},handleError:function(F,H,E,G){if(F.error){F.error(H,E,G)}if(F.global){o.event.trigger("ajaxError",[H,F,G])}},active:0,httpSuccess:function(F){try{return !F.status&&location.protocol=="file:"||(F.status>=200&&F.status<300)||F.status==304||F.status==1223}catch(E){}return false},httpNotModified:function(G,E){try{var H=G.getResponseHeader("Last-Modified");return G.status==304||H==o.lastModified[E]}catch(F){}return false},httpData:function(J,H,G){var F=J.getResponseHeader("content-type"),E=H=="xml"||!H&&F&&F.indexOf("xml")>=0,I=E?J.responseXML:J.responseText;if(E&&I.documentElement.tagName=="parsererror"){throw"parsererror"}if(G&&G.dataFilter){I=G.dataFilter(I,H)}if(typeof I==="string"){if(H=="script"){o.globalEval(I)}if(H=="json"){I=l["eval"]("("+I+")")}}return I},param:function(E){var G=[];function H(I,J){G[G.length]=encodeURIComponent(I)+"="+encodeURIComponent(J)}if(o.isArray(E)||E.jquery){o.each(E,function(){H(this.name,this.value)})}else{for(var F in E){if(o.isArray(E[F])){o.each(E[F],function(){H(F,this)})}else{H(F,o.isFunction(E[F])?E[F]():E[F])}}}return G.join("&").replace(/%20/g,"+")}});var m={},n,d=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];function t(F,E){var G={};o.each(d.concat.apply([],d.slice(0,E)),function(){G[this]=F});return G}o.fn.extend({show:function(J,L){if(J){return this.animate(t("show",3),J,L)}else{for(var H=0,F=this.length;H").appendTo("body");K=I.css("display");if(K==="none"){K="block"}I.remove();m[G]=K}o.data(this[H],"olddisplay",K)}}for(var H=0,F=this.length;H=0;H--){if(G[H].elem==this){if(E){G[H](true)}G.splice(H,1)}}});if(!E){this.dequeue()}return this}});o.each({slideDown:t("show",1),slideUp:t("hide",1),slideToggle:t("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(E,F){o.fn[E]=function(G,H){return this.animate(F,G,H)}});o.extend({speed:function(G,H,F){var E=typeof G==="object"?G:{complete:F||!F&&H||o.isFunction(G)&&G,duration:G,easing:F&&H||H&&!o.isFunction(H)&&H};E.duration=o.fx.off?0:typeof E.duration==="number"?E.duration:o.fx.speeds[E.duration]||o.fx.speeds._default;E.old=E.complete;E.complete=function(){if(E.queue!==false){o(this).dequeue()}if(o.isFunction(E.old)){E.old.call(this)}};return E},easing:{linear:function(G,H,E,F){return E+F*G},swing:function(G,H,E,F){return((-Math.cos(G*Math.PI)/2)+0.5)*F+E}},timers:[],fx:function(F,E,G){this.options=E;this.elem=F;this.prop=G;if(!E.orig){E.orig={}}}});o.fx.prototype={update:function(){if(this.options.step){this.options.step.call(this.elem,this.now,this)}(o.fx.step[this.prop]||o.fx.step._default)(this);if((this.prop=="height"||this.prop=="width")&&this.elem.style){this.elem.style.display="block"}},cur:function(F){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null)){return this.elem[this.prop]}var E=parseFloat(o.css(this.elem,this.prop,F));return E&&E>-10000?E:parseFloat(o.curCSS(this.elem,this.prop))||0},custom:function(I,H,G){this.startTime=e();this.start=I;this.end=H;this.unit=G||this.unit||"px";this.now=this.start;this.pos=this.state=0;var E=this;function F(J){return E.step(J)}F.elem=this.elem;if(F()&&o.timers.push(F)&&!n){n=setInterval(function(){var K=o.timers;for(var J=0;J=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var E=true;for(var F in this.options.curAnim){if(this.options.curAnim[F]!==true){E=false}}if(E){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(o.css(this.elem,"display")=="none"){this.elem.style.display="block"}}if(this.options.hide){o(this.elem).hide()}if(this.options.hide||this.options.show){for(var I in this.options.curAnim){o.attr(this.elem.style,I,this.options.orig[I])}}this.options.complete.call(this.elem)}return false}else{var J=G-this.startTime;this.state=J/this.options.duration;this.pos=o.easing[this.options.easing||(o.easing.swing?"swing":"linear")](this.state,J,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update()}return true}};o.extend(o.fx,{speeds:{slow:600,fast:200,_default:400},step:{opacity:function(E){o.attr(E.elem.style,"opacity",E.now)},_default:function(E){if(E.elem.style&&E.elem.style[E.prop]!=null){E.elem.style[E.prop]=E.now+E.unit}else{E.elem[E.prop]=E.now}}}});if(document.documentElement.getBoundingClientRect){o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}var G=this[0].getBoundingClientRect(),J=this[0].ownerDocument,F=J.body,E=J.documentElement,L=E.clientTop||F.clientTop||0,K=E.clientLeft||F.clientLeft||0,I=G.top+(self.pageYOffset||o.boxModel&&E.scrollTop||F.scrollTop)-L,H=G.left+(self.pageXOffset||o.boxModel&&E.scrollLeft||F.scrollLeft)-K;return{top:I,left:H}}}else{o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}o.offset.initialized||o.offset.initialize();var J=this[0],G=J.offsetParent,F=J,O=J.ownerDocument,M,H=O.documentElement,K=O.body,L=O.defaultView,E=L.getComputedStyle(J,null),N=J.offsetTop,I=J.offsetLeft;while((J=J.parentNode)&&J!==K&&J!==H){M=L.getComputedStyle(J,null);N-=J.scrollTop,I-=J.scrollLeft;if(J===G){N+=J.offsetTop,I+=J.offsetLeft;if(o.offset.doesNotAddBorder&&!(o.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(J.tagName))){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}F=G,G=J.offsetParent}if(o.offset.subtractsBorderForOverflowNotVisible&&M.overflow!=="visible"){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}E=M}if(E.position==="relative"||E.position==="static"){N+=K.offsetTop,I+=K.offsetLeft}if(E.position==="fixed"){N+=Math.max(H.scrollTop,K.scrollTop),I+=Math.max(H.scrollLeft,K.scrollLeft)}return{top:N,left:I}}}o.offset={initialize:function(){if(this.initialized){return}var L=document.body,F=document.createElement("div"),H,G,N,I,M,E,J=L.style.marginTop,K='
';M={position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"};for(E in M){F.style[E]=M[E]}F.innerHTML=K;L.insertBefore(F,L.firstChild);H=F.firstChild,G=H.firstChild,I=H.nextSibling.firstChild.firstChild;this.doesNotAddBorder=(G.offsetTop!==5);this.doesAddBorderForTableAndCells=(I.offsetTop===5);H.style.overflow="hidden",H.style.position="relative";this.subtractsBorderForOverflowNotVisible=(G.offsetTop===-5);L.style.marginTop="1px";this.doesNotIncludeMarginInBodyOffset=(L.offsetTop===0);L.style.marginTop=J;L.removeChild(F);this.initialized=true},bodyOffset:function(E){o.offset.initialized||o.offset.initialize();var G=E.offsetTop,F=E.offsetLeft;if(o.offset.doesNotIncludeMarginInBodyOffset){G+=parseInt(o.curCSS(E,"marginTop",true),10)||0,F+=parseInt(o.curCSS(E,"marginLeft",true),10)||0}return{top:G,left:F}}};o.fn.extend({position:function(){var I=0,H=0,F;if(this[0]){var G=this.offsetParent(),J=this.offset(),E=/^body|html$/i.test(G[0].tagName)?{top:0,left:0}:G.offset();J.top-=j(this,"marginTop");J.left-=j(this,"marginLeft");E.top+=j(G,"borderTopWidth");E.left+=j(G,"borderLeftWidth");F={top:J.top-E.top,left:J.left-E.left}}return F},offsetParent:function(){var E=this[0].offsetParent||document.body;while(E&&(!/^body|html$/i.test(E.tagName)&&o.css(E,"position")=="static")){E=E.offsetParent}return o(E)}});o.each(["Left","Top"],function(F,E){var G="scroll"+E;o.fn[G]=function(H){if(!this[0]){return null}return H!==g?this.each(function(){this==l||this==document?l.scrollTo(!F?H:o(l).scrollLeft(),F?H:o(l).scrollTop()):this[G]=H}):this[0]==l||this[0]==document?self[F?"pageYOffset":"pageXOffset"]||o.boxModel&&document.documentElement[G]||document.body[G]:this[0][G]}});o.each(["Height","Width"],function(I,G){var E=I?"Left":"Top",H=I?"Right":"Bottom",F=G.toLowerCase();o.fn["inner"+G]=function(){return this[0]?o.css(this[0],F,false,"padding"):null};o.fn["outer"+G]=function(K){return this[0]?o.css(this[0],F,false,K?"margin":"border"):null};var J=G.toLowerCase();o.fn[J]=function(K){return this[0]==l?document.compatMode=="CSS1Compat"&&document.documentElement["client"+G]||document.body["client"+G]:this[0]==document?Math.max(document.documentElement["client"+G],document.body["scroll"+G],document.documentElement["scroll"+G],document.body["offset"+G],document.documentElement["offset"+G]):K===g?(this.length?o.css(this[0],J):null):this.css(J,typeof K==="string"?K:K+"px")}})})(); +jQ13 = jQuery.noConflict(true); +/*jsl:end*/ \ No newline at end of file diff --git l10n_community/l10n_community.css l10n_community/l10n_community.css index a6cb165..884a053 100644 --- l10n_community/l10n_community.css +++ l10n_community/l10n_community.css @@ -62,147 +62,3 @@ table.l10n-server-filter td.last { 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; -} diff --git l10n_community/l10n_community.module l10n_community/l10n_community.module index 66d6965..9059822 100644 --- l10n_community/l10n_community.module +++ l10n_community/l10n_community.module @@ -11,19 +11,59 @@ */ /** - * No permission to suggest or translate, just view. + * Base permission; no access at all. */ -define('L10N_PERM_NONE', 0); +define('L10N_PERMISSION_NONE', 0); /** - * Permission to suggest. + * User may view the translation. */ -define('L10N_PERM_SUGGEST', 1); +define('L10N_PERMISSION_VIEW', 1); /** - * Permission both to suggest and translate. + * User may contribute suggestions. */ -define('L10N_PERM_ALL', 2); +define('L10N_PERMISSION_SUGGEST', 2); + +/** + * User may approve or decline other user's suggestions. + */ +define('L10N_PERMISSION_MODERATE_OTHERS', 4); + +/** + * User may approve her own suggestions. + */ +define('L10N_PERMISSION_MODERATE_OWN', 8); + +/** + * User may mark other user's translations as stable. + */ +define('L10N_PERMISSION_STABILIZE_OTHERS', 16); + +/** + * User may mark her translations as stable. + */ +define('L10N_PERMISSION_STABILIZE_OWN', 32); + +/** + * User may import translations from a file. + */ +define('L10N_PERMISSION_IMPORT', 64); + +/** + * User may export translation templates or translated .po files. + */ +define('L10N_PERMISSION_EXPORT', 128); + +/** + * Accumulates all permissions a contributor may have. + */ +define('L10N_PERMISSION_CONTRIBUTE', L10N_PERMISSION_SUGGEST | L10N_PERMISSION_IMPORT); + +/** + * Accumulates all permissions a reviewer may have. + */ +define('L10N_PERMISSION_REVIEW', L10N_PERMISSION_MODERATE_OTHERS | L10N_PERMISSION_MODERATE_OWN | L10N_PERMISSION_STABILIZE_OTHERS | L10N_PERMISSION_STABILIZE_OWN); /** * Strings with any status. @@ -177,28 +217,7 @@ function l10n_community_menu() { $items['translate/details'] = array( 'title' => 'String details', 'page callback' => 'l10n_community_string_details', - 'file' => 'ajax.inc', - 'access arguments' => array('access localization community'), - 'type' => MENU_CALLBACK, - ); - $items['translate/suggestions'] = array( - 'title' => 'String suggestions', - 'page callback' => 'l10n_community_string_suggestions', - 'file' => 'ajax.inc', - 'access arguments' => array('access localization community'), - 'type' => MENU_CALLBACK, - ); - $items['translate/approve'] = array( - 'title' => 'Approve suggestion', - 'page callback' => 'l10n_community_string_approve', - 'file' => 'ajax.inc', - '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', + 'file' => 'translate.inc', 'access arguments' => array('access localization community'), 'type' => MENU_CALLBACK, ); @@ -219,42 +238,23 @@ function l10n_community_menu() { 'weight' => -20, ); // Tabs to translate, import and export projects. - $items['translate/languages/%l10n_community_language/view'] = array( - 'title' => 'Browse', + $items['translate/languages/%l10n_community_language/list'] = array( + 'title' => 'Translate', 'page callback' => 'l10n_community_translate_page', 'page arguments' => array(2), 'file' => 'translate.inc', - 'access arguments' => array('access localization community'), + 'access callback' => 'l10n_community_get_permission', + 'access arguments' => array(2, NULL, L10N_PERMISSION_VIEW), '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_contribute_access', - 'access arguments' => array(2), - 'type' => MENU_LOCAL_TASK, - 'weight' => -8, - ); - $items['translate/languages/%l10n_community_language/moderate'] = array( - 'title' => 'Moderate', - 'page callback' => 'l10n_community_moderate_page', - 'page arguments' => array(2), - 'file' => 'moderate.inc', - 'access callback' => 'l10n_community_moderation_access', - 'access arguments' => array(2), - 'type' => MENU_LOCAL_TASK, - 'weight' => -6, - ); $items['translate/languages/%l10n_community_language/import'] = array( 'title' => 'Import', 'page callback' => 'l10n_community_import_page', 'page arguments' => array(2), 'file' => 'import.inc', - 'access callback' => 'l10n_community_contribute_access', - 'access arguments' => array(2, TRUE), + 'access callback' => 'l10n_community_get_permission', + 'access arguments' => array(2, NULL, L10N_PERMISSION_SUGGEST | L10N_PERMISSION_IMPORT), 'type' => MENU_LOCAL_TASK, 'weight' => -5, ); @@ -263,7 +263,8 @@ function l10n_community_menu() { 'page callback' => 'l10n_community_export_page', 'page arguments' => array(NULL, 2), 'file' => 'export.inc', - 'access callback' => 'l10n_community_export_access', + 'access callback' => 'l10n_community_get_permission', + 'access arguments' => array(2, NULL, L10N_PERMISSION_EXPORT), 'type' => MENU_LOCAL_TASK, 'weight' => 0, ); @@ -288,7 +289,8 @@ function l10n_community_menu() { 'title' => 'Export template', 'page callback' => 'l10n_community_export_page', 'page arguments' => array(2), - 'access callback' => 'l10n_community_export_access', + 'access callback' => 'l10n_community_get_permission', + 'access arguments' => array(2, NULL, L10N_PERMISSION_EXPORT), 'file' => 'export.inc', 'type' => MENU_LOCAL_TASK, 'weight' => 0, @@ -357,32 +359,6 @@ function l10n_community_project_admin_load($uri) { } /** - * Access callback for editing and import paths. Requires edit privileges for given language. - * - * @param $langcode - * Language code. - * @param $import - * Boolean. Whether this is a gettext import operation. - */ -function l10n_community_contribute_access($langcode, $import = FALSE) { - return user_access('access localization community') && (l10n_community_get_permission($langcode) != L10N_PERM_NONE) && ($import ? user_access('import gettext files') : TRUE); -} - -/** - * Access callback for exporting paths. Requires access and export privileges. - */ -function l10n_community_export_access() { - return user_access('access localization community') && user_access('export gettext templates and translations'); -} - -/** - * Access to the moderation screen. - */ -function l10n_community_moderation_access($langcode) { - return user_access('access localization community') && (l10n_community_get_permission($langcode) == L10N_PERM_ALL); -} - -/** * Title callback for project pages. */ function l10n_community_page_title_project($uri) { @@ -411,7 +387,15 @@ function l10n_community_init() { * Implementation of hook_perm(). */ function l10n_community_perm() { - return array('access localization community', 'submit suggestions', 'submit translations and approve suggestions', 'administer localization community', 'export gettext templates and translations', 'import gettext files'); + return array( + 'access localization community', + 'administer localization community', + 'export gettext templates and translations', + 'import gettext files', + 'submit translation suggestions', + 'moderate other people\'s suggestions', + 'moderate own suggestions', + ); } /** @@ -472,24 +456,18 @@ function l10n_community_block_help() { // Match actual translation editing or review pages with the two different path models they could have. if (preg_match('!translate/languages/(?P[^/]+)(/(?Pview|edit|import|export))?$!', $_GET['q'], $args) || preg_match('!translate/projects/(?P[^/]+)(/(?P|export))??$!', $_GET['q'], $args)) { - $perm = isset($args['langcode']) ? l10n_community_get_permission($args['langcode']) : L10N_PERM_NONE; + $perm = l10n_community_get_permission(isset($args['langcode']) ? $args['langcode'] : NULL); $permission_help = ''; if (module_exists('l10n_groups')) { // We are dealing with a groups based permission model. $permission_help = l10n_groups_block_help($perm, isset($args['langcode']) ? $args['langcode'] : NULL); } else { - // We are dealing with a simple permission model. - switch ($perm) { - case L10N_PERM_NONE: - // No user access to this page in this case. - break; - case L10N_PERM_SUGGEST: - $permission_help = t('You can suggest translations or import complete Gettext translation files to suggest multiple translations at once. People with permission to approve suggestions may either accept or decline your suggestions. To work offline, export a translation template, which contains the current state of the translation.'); - break; - case L10N_PERM_ALL: - $permission_help = t('As a fully empowered user, you can suggest translations as well as approve translations suggested by others. Export/import of Gettext translation files is also possible.'); - break; + if (!((L10N_PERMISSION_MODERATE_OTHERS | L10N_PERMISSION_MODERATE_OWN) & ~$perm)) { + $permission_help = t('As a fully empowered user, you can suggest translations as well as approve translations suggested by others. Export/import of Gettext translation files is also possible.'); + } + elseif ($perm & L10N_PERMISSION_SUGGEST) { + $permission_help = t('You can suggest translations or import complete Gettext translation files to suggest multiple translations at once. People with permission to approve suggestions may either accept or decline your suggestions. To work offline, export a translation template, which contains the current state of the translation.'); } $permission_help = !empty($permission_help) ? ('

'. $permission_help .'

') : ''; } @@ -894,6 +872,19 @@ function l10n_community_get_languages($key = NULL) { } /** + * Returns a language object for a specific language. + * + * @param $langcode + * Language code, for example 'hu', 'pt-br', 'de' or 'it'. + * @return + * A populated language object. + */ +function l10n_community_get_language($langcode) { + $languages = l10n_community_get_languages(); + return $languages[$langcode]; +} + +/** * Get translation permission level for a specific user. * * @@ -902,45 +893,49 @@ function l10n_community_get_languages($key = NULL) { * @param $account * Optional user account. * @return - * L10N_PERM_NONE, L10N_PERM_SUGGEST or L10N_PERM_ALL + * A bitmask of various permission flags. */ -function l10n_community_get_permission($langcode, $account = NULL) { +function l10n_community_get_permission($langcode, $account = NULL, $check = NULL) { static $permissions = array(); global $user; if (empty($langcode)) { - return L10N_PERM_NONE; + return L10N_PERMISSION_NONE; } if (!isset($account)) { $account = $user; } - if (isset($permissions[$account->uid][$langcode])) { - // Return cached value if available. - return $permissions[$account->uid][$langcode]; + if (!isset($permissions[$account->uid])) { + $permissions[$account->uid] = array(); } + if (!isset($permissions[$account->uid][$langcode])) { + // Minimal permission. + $global = L10N_PERMISSION_NONE; + $global |= user_access("access localization community", $account) ? L10N_PERMISSION_VIEW : 0; + $global |= user_access("export gettext templates and translations", $account) ? L10N_PERMISSION_EXPORT : 0; + $global |= user_access("submit translation suggestions", $account) ? L10N_PERMISSION_SUGGEST : 0; + $global |= user_access("moderate other people's suggestions", $account) ? L10N_PERMISSION_MODERATE_OTHERS : 0; + $global |= user_access("moderate own suggestions", $account) ? L10N_PERMISSION_MODERATE_OWN : 0; + $global |= user_access("mark other people's translations as stable", $account) ? L10N_PERMISSION_STABILIZE_OTHERS : 0; + $global |= user_access("mark own translations as stable", $account) ? L10N_PERMISSION_STABILIZE_OWN : 0; + $global |= user_access("import gettext files", $account) ? L10N_PERMISSION_IMPORT : 0; - // Initialize to lowest possible permission. - $permissions[$account->uid][$langcode] = L10N_PERM_NONE; - - if ($account->uid == 1) { - // The administrator has all permissions in all languages. - return ($permissions[$account->uid][$langcode] = L10N_PERM_ALL); + if (!module_exists('l10n_groups')) { + $permissions[$account->uid][$langcode] = $global; + } + else { + l10n_groups_get_permission($permissions, $account, $global); + } } - $global_permission = user_access('submit translations and approve suggestions', $account) ? L10N_PERM_ALL : (user_access('submit suggestions', $account) ? L10N_PERM_SUGGEST : L10N_PERM_NONE); - - if (($global_permission != L10N_PERM_NONE) && module_exists('l10n_groups')) { - // Fill up permission information for this account if it has any permission. - l10n_groups_get_permission($permissions, $global_permission, $langcode, $account); + if (isset($check)) { + return !(~$permissions[$account->uid][$langcode] & ($check | L10N_PERMISSION_VIEW)); } else { - // Remember global permission if using l10n_community standalone. - $permissions[$account->uid][$langcode] = $global_permission; + // Return from local cache. + return $permissions[$account->uid][$langcode]; } - - // Return from local cache. - return $permissions[$account->uid][$langcode]; } /** @@ -1150,32 +1145,23 @@ function l10n_community_trim($translation, $source) { return $matches[1] . trim($translation) . $matches[2]; } -/** - * 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) { - // 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) { +function l10n_community_update_message($declined, $approved, $inserted, $updated = 0, $unchanged = 0, $suggested = 0) { + $messages = array(); + if ($declined) + $messages[] = format_plural($declined, '1 translation declined', '@count translations declined'); + if ($approved) + $messages[] = format_plural($approved, '1 translation approved', '@count translations approved'); + if ($inserted) + $messages[] = format_plural($inserted, '1 translation added', '@count translations added'); + if ($updated) $message[] = format_plural($updated, '1 translation updated', '@count translations updated'); - } - if ($unchanged) { + 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 (count($message)) { - drupal_set_message(join(', ', $message) .'.'); - } + if ($suggested) + $message[] = format_plural($suggested, '1 new suggestion added', '@count new suggestions added'); + + if ($messages) + drupal_set_message(implode(', ', $messages)); } /** @@ -1236,14 +1222,6 @@ function l10n_community_project_uri_by_title($title) { return db_result(db_query("SELECT uri FROM {l10n_community_project} WHERE title = '%s'", $title)); } -/** - * Check whether $suggestion is duplicate for $sid in $langcode. - */ -function l10n_community_is_duplicate($suggestion, $sid, $langcode) { - // Use BINARY matching to avoid marking case-corrections as duplicate. - 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 =========================================================== /** @@ -1251,16 +1229,6 @@ function l10n_community_is_duplicate($suggestion, $sid, $langcode) { */ function l10n_community_theme($existing, $type, $theme, $path) { return array( - // l10n_community.module - 'l10n_community_button' => array( - 'arguments' => array('type' => NULL, 'class' => NULL, 'extras' => ''), - ), - 'l10n_community_strings' => array( - 'arguments' => array('items' => NULL, 'form' => TRUE), - ), - 'l10n_community_copy_button' => array( - 'arguments' => array(), - ), // pages.inc 'l10n_community_progress_columns' => array( 'arguments' => array('sum' => NULL, 'translated' => NULL, 'has_suggestion' => NULL), @@ -1275,11 +1243,20 @@ function l10n_community_theme($existing, $type, $theme, $path) { 'l10n_community_filter_form' => array( 'arguments' => array('form' => NULL), ), - 'l10n_community_translate_form' => array( - 'arguments' => array('form' => NULL), + 'l10n_community_translate_translation' => array( + 'arguments' => array('element' => NULL), + ), + 'l10n_community_translate_radio' => array( + 'arguments' => array('element' => NULL), + ), + 'l10n_community_translate_source' => array( + 'arguments' => array('element' => NULL), + ), + 'l10n_community_translate_translation_list' => array( + 'arguments' => array('element' => NULL), ), - 'l10n_community_in_context' => array( - 'arguments' => array('source' => NULL), + 'l10n_community_translate_table' => array( + 'arguments' => array('element' => NULL), ), // l10n_community.admin.inc 'l10n_community_admin_projects_form' => array( @@ -1288,152 +1265,10 @@ function l10n_community_theme($existing, $type, $theme, $path) { 'l10n_community_admin_releases_form' => array( 'arguments' => array('form' => NULL), ), - // moderate.inc - 'l10n_community_moderation_form' => array( - 'arguments' => array('form' => NULL), - ), ); } /** - * 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 = "
    "; - foreach ($items as $i => $item) { - $output .= "
  • "; - if ($form && ($i < 1)) { - // Only print copy button if we are displaying a form and on the first item. - // For plurals, only the first item will have the copy button, but will copy - // all the values into the form. - $output .= "
    ". theme('l10n_community_copy_button') ."
    "; - } - $output .= "
    ". $item ."
    "; - $output .= "
  • "; - } - $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) { - if (strpos($value, "\0") !== FALSE) { - $items = explode(chr(0), $value); - foreach ($items as &$item) { - $item = l10n_community_format_text($item, NULL, NULL, $rich_markup); - } - return theme('item_list', $items); - } - else { - return l10n_community_format_text($value, NULL, NULL, $rich_markup); - } -} - -/** - * Format translatable strings with custom icons. - * - * We emphasize some parts of strings, so those are easy to recognize. - * Newlines and replacement strings are made more visible. - * - * @param $string - * Source string to translate. - * @param $sid - * Source string ID. - * @param $delta - * Sequence ID of plural version if $string is a plural variant. - * @param $rich_markup - * Whether to output rich markup (used for the translaton UI). - */ -function l10n_community_format_text($string, $sid = NULL, $delta = NULL, $rich_markup = TRUE) { - static $path = NULL, $title = NULL; - - if (!isset($path)) { - $path = base_path() . drupal_get_path('module', 'l10n_community'); - $title = t('line break'); - } - - // Replace all newline chars in the string with an indicator image. - $formatted = str_replace( - array("\n", "\\\\n"), - ''. $title .'
', - check_plain($string) - ); - // Make all %, ! and @ marked pladeholders emphasized. - $formatted = preg_replace( - '~((%|!|@)[0-9a-zA-Z_-]+)~', - '\\1', - $formatted - ); - - if ($rich_markup) { - $class = ''; - if (isset($sid) && isset($delta)) { - $class = ' class="string-'. $sid .'-'. $delta .'"'; - } - elseif ($sid) { - $class = ' class="string-'. $sid .'"'; - } - return ''. $formatted .'
'; - } - else { - return ''. $formatted .''; - } -} - -/** * Compute language community stats. * * @param $langcode diff --git l10n_community/moderate.inc l10n_community/moderate.inc deleted file mode 100644 index 585f45a..0000000 --- l10n_community/moderate.inc +++ /dev/null @@ -1,302 +0,0 @@ - $languages[$langcode]->name))); - $output .= drupal_get_form('l10n_community_moderation_form', $strings, $languages[$langcode], $filters); - } - return $output; -} - -// == Moderation form ========================================================== - -/** - * Translation web interface. - * - * @param $strings - * Array of string objects to display. - * @param $language - * Language object. - * @param $filters - * Filters used to present this moderation view. - */ -function l10n_community_moderation_form(&$form_state, $strings = array(), $language = NULL, $filters) { - $form['pager'] = array( - '#value' => theme('pager', NULL, $filters['limit'], 0) - ); - - // Keep language code in form for further reference. - $form['langcode'] = array( - '#type' => 'value', - '#value' => $language->language - ); - - // Operations. - $form['options'] = array( - '#type' => 'fieldset', - '#title' => t('Update options'), - '#prefix' => '
', - '#suffix' => '
', - ); - $form['options']['approve'] = array( - '#type' => 'submit', - '#value' => t('Approve all selected'), - ); - $form['options']['decline'] = array( - '#type' => 'submit', - '#value' => t('Decline all selected'), - ); - - // All strings and suggestions - $form['strings'] = array( - '#tree' => TRUE, - ); - foreach ($strings as $string) { - $form['strings']['tid'][$string->tid] = array( - '#type' => 'checkbox', - ); - $form['strings']['sid'][$string->tid] = array( - '#type' => 'value', '#value' => $string->sid, - ); - $form['strings']['source'][$string->tid] = array( - //'#type' => 'item', - '#value' => l10n_community_format_string($string->source, FALSE) .' '. theme('l10n_community_in_context', $string), - ); - $form['strings']['translation'][$string->tid] = array( - //'#type' => 'item', - '#value' => l10n_community_format_string($string->translation, FALSE), - ); - $form['strings']['suggestion'][$string->tid] = array( - //'#type' => 'item', - '#value' => l10n_community_format_string($string->suggestion, FALSE), - ); - } - return $form; -} - -/** - * Moderation form submission callback. - */ -function l10n_community_moderation_form_submit($form, &$form_state) { - // Filter out unchecked translations. - $translations = array_filter($form_state['values']['strings']['tid']); - if (!empty($translations)) { - // Use these tids to get the list of sids. - $strings = array_intersect_key($form_state['values']['strings']['sid'], $translations); - - // The tids are the keys of the array. - $tids = array_keys($translations); - // The strings array has tid => sid. - $sids = array_unique($strings); - if ($form_state['values']['op'] == t('Approve all selected')) { - l10n_community_moderate_approve_selected($form_state['values']['langcode'], $tids, $sids); - } - elseif ($form_state['values']['op'] == t('Decline all selected')) { - l10n_community_moderate_decline_selected($form_state['values']['langcode'], $tids, $sids); - } - } - else { - drupal_set_message(t('No suggestions selected for operation. Please select one or more suggestions to run the operation on.'), 'error'); - } -} - - -// == API functions ============================================================ - -/** - * Get suggestions based on some conditions. - * - * @param $langcode - * Language code, for example 'hu', 'pt-br', 'de', 'it'. - * @param $filters - * See l10n_community_get_strings(). - * @return - * An array of suggestion records from database. - */ -function l10n_community_get_suggestions($langcode, $filters) { - $join = $join_args = $where = $where_args = array(); - $sql = $sql_count = ''; - - $select = "SELECT DISTINCT ts.tid, ts.sid, tt.translation, ts.translation AS suggestion, s.value AS source, s.context FROM {l10n_community_translation} ts"; - $select_count = "SELECT COUNT(DISTINCT(ts.tid)) FROM {l10n_community_translation} ts"; - $join[] = "INNER JOIN {l10n_community_string} s ON ts.sid = s.sid"; - $where[] = "ts.is_suggestion = 1 AND ts.is_active = 1 AND ts.language = '%s'"; - $where_args[] = $langcode; - - // Add submitted by condition - if (!empty($filters['author'])) { - $where[] = "ts.uid_entered = %d"; - $where_args[] = $filters['author']->uid; - } - - // Restrict based on project or release. - $release = empty($filters['release']) || $filters['release'] === 'all' ? NULL : $filters['release']; - $project = $filters['project']; - if ($release || $project) { - $join[] = "INNER JOIN {l10n_community_line} l ON ts.sid = l.sid"; - // If we have a release we ignore the project - if ($release) { - // Release restriction. - $where_args[] = $release; - $where[] = 'l.rid = %d'; - } - elseif ($project) { - $where[] = "l.pid = %d"; - $where_args[] = $project->pid; - } - } - - // Context based filtering. - if (isset($filters['context']) && $filters['context'] != 'all') { - // We use 'none' for no context, so '' can be the defaut (for all contexts). - $where_args[] = $filters['context'] == 'none' ? '' : $filters['context']; - $where[] = "s.context = '%s'"; - } - - // Search in the source or target (suggestion) strings. - if (!empty($filters['search'])) { - $where_args[] = $filters['search']; - $where_args[] = $filters['search']; - $where[] = "(s.value LIKE '%%%s%%' OR ts.translation LIKE '%%%s%%')"; - } - - // Restriction based on string status. - if ($filters['status'] & L10N_STATUS_UNTRANSLATED) { - $join[] = "LEFT JOIN {l10n_community_translation} tt ON tt.sid = ts.sid AND tt.is_suggestion = 0 AND tt.is_active = 1 AND tt.language = '%s'"; - $join_args[] = $langcode; - $where[] = "(tt.tid IS NULL OR tt.translation = '')"; - } - elseif ($filters['status'] & L10N_STATUS_TRANSLATED) { - $join[] = "INNER JOIN {l10n_community_translation} tt ON tt.sid = ts.sid AND tt.is_suggestion = 0 AND tt.is_active = 1 AND tt.language = '%s'"; - $join_args[] = $langcode; - } - else { - $join[] = "LEFT JOIN {l10n_community_translation} tt ON tt.sid = ts.sid AND tt.is_suggestion = 0 AND tt.is_active = 1 AND tt.language = '%s'"; - $join_args[] = $langcode; - } - - // Build the queries - $sql_args = array_merge($join_args, $where_args); - $sql_where = implode(' ', $join) .' WHERE '. implode(' AND ', $where); - $sql = $select .' '. $sql_where; - $sql_count = $select_count .' '. $sql_where; - - $strings = pager_query($sql, $filters['limit'], 0, $sql_count, $sql_args); - $result = array(); - while ($string = db_fetch_object($strings)) { - $result[] = $string; - } - return $result; -} - -/** - * Mass approve callback. - */ -function l10n_community_moderate_approve_selected($langcode, $tids, $strings) { - global $user; - - // In case we have more than one sugggestion for an sid, error. - $sids = array_unique($strings); - if (count($sids) < count($strings)) { - drupal_set_message(t('You cannot approve more than one suggestion per string.'), 'error'); - return; - } - $tid_placeholders = db_placeholders($tids); - $sid_placeholders = db_placeholders($sids); - - // Mark existing translations and suggestions as inactive in this language. - $args = array_merge($sids, array($langcode)); - db_query("UPDATE {l10n_community_translation} SET is_active = 0 WHERE sid IN ($sid_placeholders) AND language = '%s'", $args); - - // Remove placeholder translation record (which was there if - // first came suggestions, before an actual translation). - db_query("DELETE FROM {l10n_community_translation} WHERE sid IN ($sid_placeholders) AND translation = '' AND language = '%s'", $args); - - // Mark this exact suggestions as active, and set approval time. - $args = array_merge(array(time(), $user->uid), $tids); - db_query("UPDATE {l10n_community_translation} SET time_approved = %d, uid_approved = %d, has_suggestion = 0, is_suggestion = 0, is_active = 1 WHERE tid IN ($tid_placeholders)", $args); - - drupal_set_message(format_plural(count($tids), 'A suggestion has been approved.', '@count suggestions have been approved.')); -} - -/** - * Mass decline callback. - */ -function l10n_community_moderate_decline_selected($langcode, $tids, $sids) { - // We are not interested in duplicated sids. - $sids = array_unique($sids); - $tid_placeholders = db_placeholders($tids); - $sid_placeholders = db_placeholders($sids); - - // Deactive the selected suggestions. - db_query("UPDATE {l10n_community_translation} SET is_active = 0 WHERE tid IN ($tid_placeholders)", $tids); - drupal_set_message(format_plural(db_affected_rows(), 'A suggestion has been declined.', '@count suggestions have been declined.')); - - // Update 'has suggestion' option for remaining string translations - $args = array_merge(array($langcode), $sids); - $result = db_query( - "SELECT tt.tid - FROM {l10n_community_translation} tt - LEFT JOIN {l10n_community_translation} ts ON tt.sid = ts.sid AND tt.language = ts.language AND ts.is_active = 1 AND ts.is_suggestion = 1 - WHERE tt.is_active = 1 AND tt.is_suggestion = 0 AND tt.has_suggestion = 1 AND tt.language = '%s' AND tt.sid IN ($sid_placeholders) - GROUP BY tt.tid - HAVING COUNT(ts.tid) = 0", - $args - ); - while ($string = db_fetch_object($result)) { - db_query("UPDATE {l10n_community_translation} SET has_suggestion = 0 WHERE tid = %d", $string->tid); - } -} - -// == Theme functions ========================================================== - -/** - * Theme the approval form - */ -function theme_l10n_community_moderation_form($form) { - $output = ''; - $pager = isset($form['pager']) ? drupal_render($form['pager']) : ''; - $output .= $pager; - $output .= drupal_render($form['options']); - $header = array(theme('table_select_header_cell'), t('Suggestion'), t('Translation'), t('Source')); - foreach (element_children($form['strings']['tid']) as $tid) { - $rows[] = array( - drupal_render($form['strings']['tid'][$tid]), - drupal_render($form['strings']['suggestion'][$tid]), - drupal_render($form['strings']['translation'][$tid]), - array('class' => 'source', 'data' => drupal_render($form['strings']['source'][$tid])), - ); - } - $output .= theme('table', $header, $rows); - $output .= $pager; - $output .= drupal_render($form); - return $output; -} diff --git l10n_community/translate.inc l10n_community/translate.inc index 54f4d37..373f165 100644 --- l10n_community/translate.inc +++ l10n_community/translate.inc @@ -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_NONE || $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 ====================================================== /** @@ -197,391 +152,546 @@ function l10n_community_filter_form_submit($form, &$form_state) { } } -// = Translation viewer ======================================================== - /** - * Form for translations display. - * - * @param $strings - * Array of string objects to display on the page. - * @param $language - * Language object corresponding to the page displayed. - * @param $filters - * Filters used to present this listing view. + * Theme function for l10n_community_filter_form. */ -function l10n_community_translate_view($strings = array(), $language = NULL, $filters = array()) { - $output = ''; - $rows = array(); - foreach ($strings as $string) { - $row = array(); - // Source display - $source = l10n_community_format_string($string->value); - $source .= theme('l10n_community_in_context', $string); - $row[] = array('data' => $source, 'class' => 'source'); - - // Translation display. - if (!empty($string->translation)) { - if (strpos(chr(0), $string->value) !== 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. + * An array of filters applied to the strings. + * @param $strings + * The strings to render. */ -function l10n_community_translate_form(&$form_state, $strings = array(), $language = NULL, $filters = array(), $perm = L10N_PERM_SUGGEST) { +function l10n_community_translate_form(&$form_state, $language, $filters, $strings) { + $permission = l10n_community_get_permission($language->language); + $pager = theme('pager', NULL, $filters['limit'], 0); + $redirect_url = $_GET; + unset($redirect_url['q']); - if (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']; + $form = array( + '#submit' => array('l10n_community_translate_submit'), + '#redirect' => array($_GET['q'], $redirect_url), + 'langcode' => array('#type' => 'value', '#value' => $language->language), + 'pager_top' => array('#weight' => -10, '#value' => $pager), + 'submit_top' => array('#type' => 'submit', '#value' => t('Save changes'), '#access' => $permission !== L10N_PERMISSION_VIEW), + 'strings' => array('#tree' => TRUE, '#theme' => 'l10n_community_translate_table'), + 'submit' => array('#type' => 'submit', '#value' => t('Save changes'), '#access' => $permission !== L10N_PERMISSION_VIEW), + 'pager_bottom' => array('#weight' => 10, '#value' => $pager), + ); + + foreach ($strings as $string) { + $form['strings'][$string->sid] = _l10n_community_translate_string($form_state, $string, $language, $permission); } - // Replace some values by their string representation for URL redirects. - foreach (array('project' => 'uri', 'author' => 'name') as $name => $key) { - if (!empty($filters[$name])) { - $filters[$name] = $filters[$name]->$key; - } + + return $form; +} + +/** + * 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); + $form = array( - '#tree' => TRUE, - '#redirect' => array($_GET['q'], $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 + '#string' => $source, + '#langcode' => $language->language, + 'source' => array('#value' => _l10n_community_translate_render_string($source->value)) ); - foreach ($strings as $string) { - $form[$string->sid] = array( - '#tree' => TRUE, - ); + // Add the current string (either a approved translation or a mock object + // for the "untranslated" string). + $form[$source->tid] = _l10n_community_translate_translation($form_state, $source, $permission, $source); + + // When there are suggestions, load them from the database. + if ($source->has_suggestion) { + $result = db_query("SELECT t.tid, t.sid, t.translation, t.uid_entered, t.time_entered, t.is_active, t.is_suggestion, u.name as username FROM {l10n_community_translation} t LEFT JOIN {users} u ON u.uid = t.uid_entered WHERE t.language = '%s' AND t.sid = %d AND t.is_active = 1 AND t.is_suggestion = 1 ORDER BY t.time_entered", $language->language, $source->sid); + while ($suggestion = db_fetch_object($result)) { + $suggestion->translation = l10n_community_unpack_string($suggestion->translation); + // Add the suggestion to the list. + $form[$suggestion->tid] = _l10n_community_translate_translation($form_state, $suggestion, $permission, $source); + } + } - // 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' => "
", - ); + // If the user may add new suggestions, display a textarea. + if ($permission & L10N_PERMISSION_SUGGEST) { + $textarea = _l10n_community_translate_translation_textarea($source, $language); + $form[$textarea->tid] = _l10n_community_translate_translation($form_state, $textarea, $permission, $source); + } - $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); + return $form; +} - $form[$string->sid]['source'] = array( - '#type' => 'item', - '#value' => $source, - ); +// Build mock object for new textarea +function _l10n_community_translate_translation_textarea($source, $language) { + global $user; - $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' => '', - ); + 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, + ); +} - if ($is_plural) { - // 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), +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_PERMISSION_MODERATE_OWN : L10N_PERMISSION_MODERATE_OTHERS); + $may_stabilize = $permission & ($is_own ? L10N_PERMISSION_STABILIZE_OWN : L10N_PERMISSION_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 ($permission & L10N_PERMISSION_SUGGEST) { + $form['edit'] = array( + '#value' => t('Edit Copy'), + '#prefix' => '', ); } + if (isset($string->username)) { + $now = time(); + $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($now - $string->time_entered))), + ); - $string_parts = explode(chr(0), $string->value); - - 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_SUGGEST) ? t('New suggestion for variant #%d', array('%d' => $i)) : t('New translation for variant #%d', array('%d' => $i)); + if ($string->is_suggestion) { + $title = t('suggested by !author on @date', $params); } else { - // Not translated yet, so we ask for initial translation or suggestion. - $description = ($perm == L10N_PERM_SUGGEST) ? t('Suggestion for variant #%d', array('%d' => $i)) : t('Translation for variant #%d', array('%d' => $i)); + $title = t('translated by !author on @date', $params); + } + if (!empty($string->username_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($now - $string->time_approved))), + )); } - // Include editing area for each plural variant. - $form[$string->sid]['translation']['value'][$i] = array( - // Use textarea for long and multiline strings. - '#type' => ((strlen($string_parts[$i]) > 45) || (count(explode("\n", $string_parts[$i])) > 1)) ? 'textarea' : 'textfield', - '#description' => $description, - '#rows' => 1, - '#id' => 'l10n-community-translation-'. $target, + $form['author'] = array( + '#value' => $title, ); } } + } - // Dealing with a simple string (no plurals). + return $form; +} - else { - if ($translated) { - $form[$string->sid]['translation_existing'] = array( - '#type' => 'item', - '#value' => theme('l10n_community_strings', array(l10n_community_format_text($string->translation, $string->sid))), - ); - } - $form[$string->sid]['translation']['value'] = array( - // Use textarea for long and multiline strings. - '#type' => ((strlen($string->value) > 45) || (count(explode("\n", $string->value)) > 1)) ? 'textarea' : 'textfield', - // Provide accurate title based on previous data and permission. - '#description' => $translated ? (($perm == L10N_PERM_SUGGEST) ? t('Add a new suggestion') : t('Add a new translation')) : (($perm == L10N_PERM_SUGGEST) ? 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; + +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'; } - } + } - // 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', - ); + $output = ''; - if ($perm == L10N_PERM_SUGGEST) { - // User with suggestion capability only, record this. - $form[$string->sid]['translation']['is_suggestion'] = array( - '#type' => 'value', - '#value' => TRUE - ); - } - else { - // User with full privileges, offer option to submit suggestion. - $form[$string->sid]['translation']['is_suggestion'] = array( - '#title' => t('Suggestion for discussion'), - '#type' => 'checkbox', - ); + $actions = ''; + foreach (array('declined', /*'stable', */'edit') as $type) { + if (isset($element[$type])) { + $actions .= '
  • '. drupal_render($element[$type]) .'
  • '; } } + if (!empty($actions)) { + $output .= '
      '. $actions .'
    '; + } - // 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' - ); + $output .= drupal_render($element['active']); - // Let the user submit the form. - $form['submit'] = array( - '#type' => 'submit', - '#value' => ($perm == L10N_PERM_SUGGEST) ? t('Save suggestions') : t('Save translations') - ); + if (isset($element['author'])) { + $output .= '
    '. drupal_render($element['author']) .'
    '; + } - $form['#theme'] = 'l10n_community_translate_form'; + if (isset($element['value'])) { + $output .= drupal_render($element['value']); + } - return $form; + return $output . ''; } -/** - * Save translations entered in the web form. - */ -function l10n_community_translate_form_submit($form, &$form_state) { - global $user; +function theme_l10n_community_translate_radio($element) { + _form_set_class($element, array('form-radio')); + $output = ''; + + if (isset($element['#title'])) { + $output .= ''; + } - $inserted = $updated = $unchanged = $suggested = $duplicates = 0; + return $output; +} - 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; +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 .= '
    '; - $text = ''; - if (is_string($item['translation']['value'])) { - // Single string representation: simple translation. - $text = $item['translation']['value']; - } - 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 = ''; - } - } + return $output; +} + +function theme_l10n_community_translate_source($element) { + $output = ''; + $output .= ''; + return $output; +} + +function theme_l10n_community_translate_table($element) { + $header = array( + t('Source Text'), + t('Translations'), + ); + + $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])), + ); + } - if (!empty($text)) { - // Check for duplicate translation or suggestion. - if (l10n_community_is_duplicate($text, $sid, $form_state['values']['langcode'])) { - $duplicates++; - continue; + + return theme('table', $header, $rows, array('class' => 'l10n-table')); +} + + +function l10n_community_translate_submit($form, &$form_state) { + $declined = 0; + $approved = 0; + $inserted = 0; + // $stabilized = 0; + + $langcode = $form_state['values']['langcode']; + + foreach ($form_state['values']['strings'] as $sid => $string) { + foreach ($string as $tid => $options) { + // Store new suggestion. + $empty_values = 0; + if (isset($options['value']) && is_array($options['value'])) { + foreach ($options['value'] as $key => $value) { + if ($value === t('')) { + $options['value'] = ''; + $empty_values++; + } + } + if ($tid === 'new' && count($options['value']) > $empty_values) { + $tid = l10n_community_add_suggestion($langcode, $sid, $options['value']); + if ($tid) { + $inserted++; + if ($string['active'] === 'new') { + $string['active'] = $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 - ); + if (is_numeric($tid) && $tid > 0) { + if ($tid == $string['active']) { + if ($options['original']->is_suggestion) { + l10n_community_approve_string($langcode, $sid, $tid); + $approved++; + } + // if (/*!$options['original']->is_stable && */!empty($options['stable'])) { + // dsm('mark as stable'); + // $stabilized++; + // } + } + elseif (!empty($options['declined'])) { + // also remove stable flag! + $declined++; + l10n_community_decline_string($langcode, $sid, $tid); + } + } } } - // Inform user about changes made to the database. - l10n_community_update_message($inserted, $updated, $unchanged, $suggested, $duplicates); + l10n_community_update_message($declined, $approved, $inserted); } -// = 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; } - $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; + + // Look for an existing active translation, if any. + $existing = db_fetch_object(db_query("SELECT tid FROM {l10n_community_translation} WHERE sid = %d AND language = '%s' AND translation = '%s'", $sid, $langcode, $translation)); + if (!empty($existing)) { + // The translation is already in the db. Make it an active suggestion again. + db_query("UPDATE {l10n_community_translation} SET is_suggestion = 1, is_active = 1 WHERE tid = %d", $existing->tid); + $tid = $existing->tid; + } + else { + // This is a new translation. + l10n_community_mock_translation($langcode, $sid); + // Insert the new suggestion. + db_query("INSERT INTO {l10n_community_translation} (sid, translation, language, uid_entered, time_entered, is_suggestion, is_active) VALUES (%d, '%s', '%s', %d, %d, 1, 1)", $sid, $translation, $langcode, $user->uid, time()); + $tid = db_last_insert_id('l10n_community_translation', 'tid'); + } + + // Mark the existing or mock translation has having suggestions. + l10n_community_update_suggestion_status($langcode, $sid); + + return $tid; +} + +/** + * Marks a translation as declined. + * + * @param $langcode + * The language of the declined translation. + * @param $sid + * The string ID the translation belongs to. + * @param $tid + * The translation ID of the translation. + */ +function l10n_community_decline_string($langcode, $sid, $tid) { + // Mark this translation as inactive. + db_query("UPDATE {l10n_community_translation} SET is_suggestion = 0, is_active = 0 WHERE tid = %d", $tid); + + // Make sure the mock translation is there in case we declined the active translation. + l10n_community_mock_translation($langcode, $sid); + l10n_community_update_suggestion_status($langcode, $sid); } /** + * Updates the has_suggestion flag for the active translation. + * + * @param $langcode + * The language of the string. + * @param $sid + * The string ID that should be updated. + */ +function l10n_community_update_suggestion_status($langcode, $sid) { + // Let's see if we have any suggestions remaining in this language. + $count = db_result(db_query("SELECT COUNT(*) FROM {l10n_community_translation} + WHERE sid = %d AND is_suggestion = 1 AND is_active = 1 AND language = '%s'", $sid, $langcode)); + + // Update the status according to the number of suggestions. + db_query("UPDATE {l10n_community_translation} SET has_suggestion = %d + WHERE sid = %d AND is_suggestion = 0 AND is_active = 1 AND language = '%s'", $count ? 1 : 0, $sid, $langcode); +} + +/** + * Marks a translation as approve. + * + * @param $langcode + * The language of the approved translation. + * @param $sid + * The string ID the translation belongs to. + * @param $tid + * The translation ID of the translation. + */ +function l10n_community_approve_string($langcode, $sid, $tid) { + global $user; + + // Remove placeholder translation record (which was there if + // first came suggestions, before an actual translation). + db_query("DELETE FROM {l10n_community_translation} WHERE sid = %d AND translation = '' AND language = '%s'", $sid, $langcode); + + // Make the existing approved string a suggestion. + db_query("UPDATE {l10n_community_translation} SET is_suggestion = 1 WHERE sid = %d AND language = '%s' AND is_suggestion = 0 AND is_active = 1", $sid, $langcode); + + // Mark this exact suggestion as active, and set approval time. + db_query("UPDATE {l10n_community_translation} SET time_approved = %d, uid_approved = %d, is_suggestion = 0, is_active = 1 WHERE tid = %d;", time(), $user->uid, $tid); + l10n_community_update_suggestion_status($langcode, $sid); +} + +/** + * Unpacks a string as retrieved from the database. + * + * @param $string + * The string with separation markers (NULL byte) + * @return + * An array of strings with one element for each plural form in case of + * a plural string, or one element in case of a regular string. + */ +function l10n_community_unpack_string($string) { + return explode("\0", $string); +} + +/** + * Packs a string for storage in the database. + * + * @param $string + * An array of strings. + * @return + * A packed string with NULL bytes separating each string. + */ +function l10n_community_pack_string($strings) { + return implode("\0", $strings); +} + +// = Miscellaneous ============================================================= + +/** * Theme context information for source strings. * * @param $string @@ -626,10 +736,12 @@ function l10n_community_get_strings($langcode, $filters, $pager = NULL) { $join = $join_args = $where = $where_args = array(); $sql = $sql_count = ''; - $select = "SELECT DISTINCT s.sid, s.value, s.context, t.tid, t.language, t.translation, t.uid_entered, t.uid_approved, t.time_entered, t.time_approved, t.has_suggestion, t.is_suggestion, t.is_active FROM {l10n_community_string} s"; + $select = "SELECT DISTINCT s.sid, s.value, s.context, t.tid, t.language, t.translation, t.uid_entered, t.uid_approved, t.time_entered, t.time_approved, t.has_suggestion, t.is_suggestion, t.is_active, u.name as username, u2.name as username_approved FROM {l10n_community_string} s"; $select_count = "SELECT COUNT(DISTINCT(s.sid)) FROM {l10n_community_string} s"; $join[] = "LEFT JOIN {l10n_community_translation} t ON s.sid = t.sid AND t.language = '%s' AND t.is_active = 1 AND t.is_suggestion = 0"; $join_args[] = $langcode; + $join[] = "LEFT JOIN {users} u ON u.uid = t.uid_entered"; + $join[] = "LEFT JOIN {users} u2 ON u2.uid = t.uid_approved"; // Add submitted by condition if (!empty($filters['author'])) { @@ -670,7 +782,7 @@ function l10n_community_get_strings($langcode, $filters, $pager = NULL) { // Restriction based on string status by translation / suggestions. $status_sql = ''; - if ($filters['status'] & L10N_STATUS_UNTRANSLATED) { + if (isset($filters['status']) && $filters['status'] & L10N_STATUS_UNTRANSLATED) { // We are doing a LEFT JOIN especially to look into the case, when we have nothing // to match in the translation table, but we still have the string. (We get our // records in the result set in this case). The translation field is empty or @@ -681,7 +793,7 @@ function l10n_community_get_strings($langcode, $filters, $pager = NULL) { 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 @@ -765,3 +877,58 @@ function l10n_community_build_filter_values($params, $suggestions = FALSE) { } 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); + $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; +} + diff --git l10n_community/welcome.inc l10n_community/welcome.inc index 31ad5d0..33df304 100644 --- l10n_community/welcome.inc +++ l10n_community/welcome.inc @@ -177,7 +177,7 @@ function l10n_community_pick_go_submit($form, &$form_state) { if (!empty($form_state['values']['project']) && ($uri = l10n_community_project_uri_by_title($form_state['values']['project']))) { // Project surely selected, possibly with language too. if (!empty($form_state['values']['langcode'])) { - $action = l10n_community_contribute_access($form_state['values']['langcode']) ? 'edit' : 'view'; + $action = l10n_community_get_permission($form_state['values']['langcode'], NULL, L10N_PERMISSION_SUGGEST) ? 'edit' : 'view'; drupal_goto('translate/languages/'. $form_state['values']['langcode'] .'/'. $action, 'project='. $uri); } else { diff --git l10n_groups/l10n_groups.module l10n_groups/l10n_groups.module index aa39d10..bc3b9de 100644 --- l10n_groups/l10n_groups.module +++ l10n_groups/l10n_groups.module @@ -242,17 +242,15 @@ function l10n_groups_block_help($perm, $langcode = NULL) { $groups = l10n_groups_get_groups(); $permission_help = ''; - switch ($perm) { - // Inform user about her permission level in this group. - case L10N_PERM_NONE: - $permission_help = t('You are not a member of this translation group, but you can still view the existing translations and export templates or translations for your own use.') .' '. ($user->uid ? t('Subscribe to this group if you would like to help out.', array('@group' => url('node/'. $groups[$langcode]->nid))) : t('Create an account or log in and subscribe to this group if you would like to help out.', array('@register' => url('user'), '@group' => url('node/'. $groups[$langcode]->nid)))); - break; - case L10N_PERM_SUGGEST: - $permission_help = t('You are a member of this translation group, so you can suggest translations or import complete Gettext translation files to suggest multiple translations at once. Administrators of this group either accept or decline suggestions provided by members of the group. To work offline, export a translation template, which contains the current state of the translation.'); - break; - case L10N_PERM_ALL: - $permission_help = t('As a member of this translation group, you can suggest translations as well as approve translations suggested by other members of the group. Export/import of Gettext translation files is also possible.'); - break; + // Inform user about her permission level in this group. + if (!((L10N_PERMISSION_MODERATE_OTHERS | L10N_PERMISSION_MODERATE_OWN | L10N_PERMISSION_SUGGEST) & ~$perm)) { + $permission_help = t('As a member of this translation group, you can suggest translations as well as approve translations suggested by other members of the group. Export/import of Gettext translation files is also possible.'); + } + elseif (L10N_PERMISSION_SUGGEST & $perm) { + $permission_help = t('You are a member of this translation group, so you can suggest translations or import complete Gettext translation files to suggest multiple translations at once. Administrators of this group either accept or decline suggestions provided by members of the group. To work offline, export a translation template, which contains the current state of the translation.'); + } + elseif (L10N_PERMISSION_VIEW & $perm) { + $permission_help = t('You are not a member of this translation group, but you can still view the existing translations and export templates or translations for your own use.') .' '. ($user->uid ? t('Subscribe to this group if you would like to help out.', array('@group' => url('node/'. $groups[$langcode]->nid))) : t('Create an account or log in and subscribe to this group if you would like to help out.', array('@register' => url('user'), '@group' => url('node/'. $groups[$langcode]->nid)))); } return '

    '. $permission_help .'

    '; } @@ -280,36 +278,33 @@ function l10n_groups_get_groups() { /** * Get permission level for a specific user based on group membership. * - * The group permissio model is taken into account and membership of + * The group permission model is taken into account and membership of * the user in language groups is checked. * * @param $permissions * Permission cache array to tunnel cache values to. - * @param $global_permission - * Permission based on user access settings to consider as a base. - * @param $langcode - * Language code, for example 'hu', 'pt-br', 'de' or 'it'. * @param $account * Optional user account. + * @param $global + * Permission based on user access settings to consider as a base. */ -function l10n_groups_get_permission(&$permissions, $global_permission, $langcode, $account) { +function l10n_groups_get_permission(&$permissions, $account, $global) { if ($groups = l10n_groups_get_groups()) { // Fill up local cache with all langcode permissions of this account for // future reference (eg. a page showing links for all languages). foreach ($groups as $group) { - $permission = L10N_PERM_NONE; - if (!empty($account->og_groups) and !empty($account->og_groups[$group->nid])) { - if (!empty($account->og_groups[$group->nid]['is_admin'])) { - // Administrators of groups have all permissions in the group. - $permission = L10N_PERM_ALL; - } - elseif ($account->og_groups[$group->nid]['is_active']) { - // Members in the group have a permission depending on the model used. - $permission = ($group->model == L10N_MODEL_OPEN ? L10N_PERM_ALL : L10N_PERM_SUGGEST); - } + $permission = $global; + + if (empty($account->og_groups) || empty($account->og_groups[$group->nid])) { + // The user is not in the group and may therefore only view. + $permission = $permission & ~(L10N_PERMISSION_CONTRIBUTE | L10N_PERMISSION_REVIEW); } - // Save the lowest common denominator of permissions given. - $permissions[$account->uid][$group->language] = min($permission, $global_permission); + elseif (empty($account->og_groups[$group->nid]['is_admin']) && $group->model != L10N_MODEL_OPEN) { + // Non-Admins of group don't have these permissions. + $permission = $permission & ~(L10N_PERMISSION_REVIEW); + } + + $permissions[$account->uid][$group->language] = $permission; } } } diff --git l10n_remote/l10n_remote.module l10n_remote/l10n_remote.module index a5496ee..c535a19 100644 --- l10n_remote/l10n_remote.module +++ l10n_remote/l10n_remote.module @@ -127,7 +127,7 @@ function l10n_remote_xmlrpc_string_submit($langcode, $source, $translation, $uid watchdog('l10n_community', 'Language not allowed for remote submission.', NULL, WATCHDOG_WARNING); return array('status' => FALSE, 'reason' => 'Language not accepted.'); } - elseif (l10n_community_get_permission($langcode, $account) == L10N_PERM_NONE) { + elseif (!l10n_community_get_permission($langcode, $account, L10N_PERMISSION_SUGGEST)) { watchdog('l10n_community', 'Not allowed to submit translations in this language remotely.', NULL, WATCHDOG_WARNING); return array('status' => FALSE, 'reason' => 'Not allowed to submit translations in this language.'); }