Merge lp:~adiroiban/launchpad/bug-359180-take-2 into lp:launchpad

Proposed by Adi Roiban
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~adiroiban/launchpad/bug-359180-take-2
Merge into: lp:launchpad
Diff against target: 802 lines (+446/-68)
11 files modified
lib/canonical/launchpad/javascript/translations/pofile.js (+306/-2)
lib/canonical/launchpad/templates/batchnavigator-navigation-links.pt (+4/-0)
lib/lp/translations/browser/pofile.py (+20/-2)
lib/lp/translations/browser/tests/pofile-views.txt (+20/-2)
lib/lp/translations/browser/translationmessage.py (+24/-0)
lib/lp/translations/stories/standalone/xx-pofile-translate-newlines-check.txt (+2/-2)
lib/lp/translations/stories/standalone/xx-pofile-translate.txt (+33/-5)
lib/lp/translations/templates/currenttranslationmessage-translate-one.pt (+8/-11)
lib/lp/translations/templates/pofile-translate.pt (+10/-43)
lib/lp/translations/templates/translationmessage-translate.pt (+7/-0)
lib/lp/translations/templates/translations-macros.pt (+12/-1)
To merge this branch: bzr merge lp:~adiroiban/launchpad/bug-359180-take-2
Reviewer Review Type Date Requested Status
Graham Binns (community) code, js Approve
Review via email: mp+20122@code.launchpad.net

This proposal supersedes a proposal from 2010-01-20.

Commit message

Add keybindings on translating pofiles or translation messages.

Description of the change

= Bug 35180 =

Would be useful if the First - Previous - Next - Last link in the translation interface had a keyboard shortcut for fast accessing them. Don't need to be visible, but at least documented somewhere.

Maybe even the Save & Continue button.

== Proposed Fix ==
Add the following keybindings on +translate page for pofile and translaitonmessage

First field is autofocused.
Shift+Alt+b - Focus first translation field
Shift+Alt+a - First page
Shift+Alt+n - Next page
Shift+Alt+p - Previous page
Shift+Alt+l - Last page
Shift+Alt+s - Save and continue
Shift+Alt+Down - Next field
Shift+Alt+Up - Previous field
Shift+Alt+j - Next field
Shift+Alt+k - Previous field
Shift+Alt+C - Copy original text (both singular and plural)
Shift+Alt+0 - Mark current translation
Shift+Alt+NUMBER - Mark suggestion NUMBER
Shift+Alt+d - Dismiss all suggestions
Shift+Alt+r - Tick "Someone should review this translation"

== Pre-implementation notes ==

I talked with Danilo about keybindng implementation using comments for bug 35180

Previous MPs:
https://code.edge.launchpad.net/~adiroiban/launchpad/bug-359180/+merge/16422
https://code.edge.launchpad.net/~adiroiban/launchpad/bug-359180-take-2/+merge/17785

Part of this branch was already approved in previous MPs, but I have refactored most of those functions.

== Implementation notes ==

The legacy JS code from canonical/launchpad/icing/build/lp/lp.js and from pofile-tranlate.pt was moved to javascript/translations/pofile.js.

Those functions from lp.js was rewritten using YUI3. Maybe we can no delete them from lp.js.

I have added a test for Bug #513625, since that bug was produces by some changes in this branch.

== Tests ==

I was not able to produce a reasonable windmill test since it is not trivial to find the current focused node or to see if a node is focused.
This requires adding onFocus and onBlur trigger for all DOM nodes.

./bin/test -t pofile-views

== Demo and QA ==

Log in as admin or as a person with rights on adding translations to a pofile.

Go to a pofile translate page:
https://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es/+translate?start=0

The first field should have focus and you can start translating right away.

Adding a new translation should automatically select the radio button in front of it.

Try the keybindings described in the "Proposed fix" section.

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/canonical/launchpad/javascript/translations/pofile.js

== JSLint notices ==
No handlers could be found for logger "bzr"
jslint: No problem found in '/home/adi/launchpad/lp-branches/bug-359180/lib/canonical/launchpad/javascript/translations/pofile.js'.

jslint: 1 file to lint.

To post a comment you must log in.
Revision history for this message
Adi Roiban (adiroiban) wrote : Posted in a previous version of this proposal

= Bug 359180 =
This is the follow up for the current commited branch for bug 359180 as it was discovered that Shift+Alt+up and Shift+Alt+Down are used by.

== Proposed fix ==
Use Shift+Alt+j and Shift+Alt+k for navigation.

Add fields in the translations_order only if the user can edit(add, change or suggest) the translation.

== Pre-implementation notes ==
Notes can be found on the previous MP:
https://code.edge.launchpad.net/~adiroiban/launchpad/bug-359180/+merge/16422

== Implementation details ==
There was a small refactorization for getting translations_order and autofocus_html_id.

== Tests ==
I was not able to produce a reasonable windmill test since it is not trivial to find the current focused node or to see if a node is focused.
This requires adding onFocus and onBlur trigger for all DOM nodes.

The test for the view is here:
./bin/test -t pofile-views

== Demo and Q/A ==
Demo and Q/A can be found on the previous MP
https://code.edge.launchpad.net/~adiroiban/launchpad/bug-359180/+merge/16422

Instead of UP and DOWN, use j and k

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/canonical/launchpad/javascript/translations/pofile.js

== JSLint notices ==
No handlers could be found for logger "bzr"
jslint: No problem found in '/home/adi/launchpad/lp-branches/bug-359180-take-2/lib/canonical/launchpad/javascript/translations/pofile.js'.

jslint: 1 file to lint.

Revision history for this message
Paul Hummer (rockstar) wrote : Posted in a previous version of this proposal

Please change line 140 of this diff so that the page tests conform with the 80 character line rule. Other than that, thanks for looking into this.

review: Approve (code)
Revision history for this message
Adi Roiban (adiroiban) wrote :

Due to bug 513625, the original branch was removed from lp/devel.

I have added a testcase for bug 513625 and I'm resubmitting this branch for review.

Revision history for this message
Graham Binns (gmb) wrote :

After talking with Adi, it's clear that the branch is actually an update to an older branch which was landed on devel and then pulled before 10.01 was released due to issue with the keybindings. However, I'm unable to get a sane diff for the changes since then.

Adi, in order to be able to review this branch I need a sane diff for it. At the moment I can't get one because I get a bunch of conflicts when trying to merge it into devel. Here's how you can produce one for me:

 1. Branch devel.
 2. Merge your original branch into your branch of devel.
 3. Merge this branch and paste the diff into the merge proposal.

That should give us just the changes that you've made since your last branch was approved; I'll be happy to review the new diff.

review: Needs Information (code)
Revision history for this message
Adi Roiban (adiroiban) wrote :

Hi,

$ bzr branch devel merge
$ bzr branch lp:~adiroiban/launchpad/bug-359180/ merge-1
$ cd merge
$ bzr merge ../merge-1/
Nothing to do.

The original branch is part of devel... just that a new commit is reverting those changes. There was no "hard rebase" to revert the branch.

I can not get a clean diff.

Is there an option for a filter to diff to only show lines changed be me?

Maybe it will be easier to have a new full review.

Revision history for this message
Adi Roiban (adiroiban) wrote :
Download full text (24.8 KiB)

Hi,

This is the diff.
It is still quite big as the original branch was submitted 2 months ago and was one of my first patch for LP.
Meanwhile I discovered the JavaScript guidelines and looking again at the code I found many places where the previous code could be improved.

Please let me know if there are any other actions I need to do to make this branch ready for review?

=== modified file 'lib/canonical/launchpad/javascript/translations/pofile.js'
--- lib/canonical/launchpad/javascript/translations/pofile.js 2010-02-22 11:41:11 +0000
+++ lib/canonical/launchpad/javascript/translations/pofile.js 2010-02-25 00:53:26 +0000
@@ -1,7 +1,7 @@
 /** Copyright (c) 2009, Canonical Ltd. All rights reserved.
  *
  * @module lp.pofile
- * @requires event, node
+ * @requires anim, cookie, event-key, event, node
  */

 YUI.add('lp.pofile', function(Y) {
@@ -42,8 +42,48 @@
 };

+var hide_notification = function(node) {
+ var hide_anim = new Y.Anim({
+ node: node,
+ to: { height: 0,
+ marginTop: 0, marginBottom: 0,
+ paddingTop: 0, paddingBottom: 0 }
+ });
+ node.setStyle('border', 'none');
+ hide_anim.set('duration', 0.4);
+ hide_anim.on('end', function(e) {
+ node.setStyle('display', 'none');
+ });
+ hide_anim.run();
+}
+
+self.updateNotificationBox = function(e) {
+ var notice = Y.one('.important-notice-container');
+ var balloon = notice.one('.important-notice-balloon');
+ var dismiss_notice_cookie = ('translation-docs-for-' +
+ documentation_cookie);
+
+ // Check the cookie to see if the user has already dismissed
+ // the notification box for this session.
+ var already_seen = Y.Cookie.get(dismiss_notice_cookie, Boolean);
+ if (already_seen) {
+ notice.setStyle('display', 'none');
+ }
+
+ var cancel_button = notice.one(
+ '.important-notice-cancel-button');
+ // Cancel button starts out hidden. If user has JavaScript,
+ // then we want to show it.
+ cancel_button.setStyle('visibility', 'visible');
+ cancel_button.on('click', function(e) {
+ e.halt();
+ hide_notification(balloon);
+ Y.Cookie.set(dismiss_notice_cookie, true);
+ });
+};
+
+
 self.setFocus = function(field) {
- //Y.log(e.type + ":" + e.keyCode + ': ' + field);
     // if there is nofield, do nothing
     if (Y.one('#' + field)) {
         Y.one('#' + field).focus();
@@ -75,7 +115,7 @@
     // The replacement regex strips all tags from the html.
     to.set('value', unescapeHTML(
         from.get('innerHTML').replace(/<\/?[^>]+>/gi, "")));
- selectWidget(select_id);
+ selectWidgetByID(select_id);
 };

@@ -97,7 +137,6 @@
     var singular_select = translation_stem + '_translation_0_new_select';
     var translation_singular = translation_stem + '_translation_0_new';
     var translation_plural = translation_stem + '_translation_';
- //Y.log(e.type + ":" + e.keyCode + ': ' + singular_select);
     // Copy singular text
     copyOriginalTextOne(
         original_singular, translation_singular, singular_select);
@@ -112,43 +151,102 @@
 };

-var selectWidget = function(widget) {
- if (Y.one('#' + widget)) {
- Y.one('#' + widget).set('checked', true)...

Revision history for this message
Graham Binns (gmb) wrote :

Hi Adi,

I really like this branch; good job! I'm not entirely happy about the lack of Windmill tests, but I understand what you mean about it being non-trivial to write one for this case. Please file a bug about adding some Windmill tests for this, detailing the difficulties you had. It might be worth mailing the launchpad-dev list to see if anyone has any suggestions.

review: Approve (code, js)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/javascript/translations/pofile.js'
--- lib/canonical/launchpad/javascript/translations/pofile.js 2010-01-27 07:38:02 +0000
+++ lib/canonical/launchpad/javascript/translations/pofile.js 2010-02-25 13:03:21 +0000
@@ -1,7 +1,7 @@
1/** Copyright (c) 2009, Canonical Ltd. All rights reserved.1/** Copyright (c) 2009, Canonical Ltd. All rights reserved.
2 *2 *
3 * @module lp.pofile3 * @module lp.pofile
4 * @requires event, node4 * @requires anim, cookie, event-key, event, node
5 */5 */
66
7YUI.add('lp.pofile', function(Y) {7YUI.add('lp.pofile', function(Y) {
@@ -41,4 +41,308 @@
41 }41 }
42};42};
4343
44}, "0.1", {"requires": ["event", "node"]});44
45var hide_notification = function(node) {
46 var hide_anim = new Y.Anim({
47 node: node,
48 to: { height: 0,
49 marginTop: 0, marginBottom: 0,
50 paddingTop: 0, paddingBottom: 0 }
51 });
52 node.setStyle('border', 'none');
53 hide_anim.set('duration', 0.4);
54 hide_anim.on('end', function(e) {
55 node.setStyle('display', 'none');
56 });
57 hide_anim.run();
58}
59
60self.updateNotificationBox = function(e) {
61 var notice = Y.one('.important-notice-container');
62 var balloon = notice.one('.important-notice-balloon');
63 var dismiss_notice_cookie = ('translation-docs-for-' +
64 documentation_cookie);
65
66 // Check the cookie to see if the user has already dismissed
67 // the notification box for this session.
68 var already_seen = Y.Cookie.get(dismiss_notice_cookie, Boolean);
69 if (already_seen) {
70 notice.setStyle('display', 'none');
71 }
72
73 var cancel_button = notice.one(
74 '.important-notice-cancel-button');
75 // Cancel button starts out hidden. If user has JavaScript,
76 // then we want to show it.
77 cancel_button.setStyle('visibility', 'visible');
78 cancel_button.on('click', function(e) {
79 e.halt();
80 hide_notification(balloon);
81 Y.Cookie.set(dismiss_notice_cookie, true);
82 });
83};
84
85
86self.setFocus = function(field) {
87 // if there is nofield, do nothing
88 if (Y.one('#' + field)) {
89 Y.one('#' + field).focus();
90 }
91};
92
93
94var setNextFocus = function(e, field) {
95 self.setFocus(field);
96 // stopPropagation() and preventDefault()
97 e.halt();
98};
99
100
101var setPreviousFocus = function(e, field, original) {
102
103 // Original singular test is focused first to make sure
104 // it is visible when scrolling up
105 self.setFocus(original);
106 self.setFocus(field);
107 // stopPropagation() and preventDefault()
108 e.halt();
109};
110
111
112var copyOriginalTextOne = function(from_id, to_id, select_id) {
113 var from = Y.one('#' + from_id);
114 var to = Y.one('#' + to_id);
115 // The replacement regex strips all tags from the html.
116 to.set('value', unescapeHTML(
117 from.get('innerHTML').replace(/<\/?[^>]+>/gi, "")));
118 selectWidgetByID(select_id);
119};
120
121
122var copyOriginalTextPlural = function (from_id,
123 to_id_pattern, nplurals) {
124 // skip when x is 0, as that is the singular
125 for (var x = 1; x < nplurals; x++) {
126 var to_id = to_id_pattern + x + "_new";
127 var to_select = to_id_pattern + x + "_new_select";
128 copyOriginalTextOne(from_id, to_id, to_select);
129 }
130};
131
132
133var copyOriginalTextAll = function(e, original_stem, translation_stem) {
134
135 var original_singular = original_stem + '_singular';
136 var original_plural = original_stem + '_plural';
137 var singular_select = translation_stem + '_translation_0_new_select';
138 var translation_singular = translation_stem + '_translation_0_new';
139 var translation_plural = translation_stem + '_translation_';
140 // Copy singular text
141 copyOriginalTextOne(
142 original_singular, translation_singular, singular_select);
143
144 // Copy plural text if needed
145 if (Y.one('#' + translation_plural + '1')) {
146 copyOriginalTextPlural(
147 original_plural, translation_plural, plural_forms);
148 }
149 // stopPropagation() and preventDefault()
150 e.halt();
151};
152
153
154var selectWidgetByID = function(widget) {
155 var node = Y.one('#' + widget);
156 if (node) {
157 node.set('checked', true);
158 }
159};
160
161
162var toggleWidget = function(widget) {
163 var node = Y.one('#' + widget);
164 if (node) {
165 if (node.get('checked')) {
166 node.set('checked', false);
167 } else {
168 node.set('checked', true);
169 }
170 }
171};
172
173
174var selectTranslation = function(e, widget) {
175 // Don't select when tabbing, navigating and simply pressing
176 // enter to submit the form.
177 // Looks like this is not needed for Epiphany and Chromium
178 if (e.keyCode == 9 || e.keyCode == 13 ||
179 e.keyCode == 38 || e.keyCode == 40 ||
180 (e.shiftKey && e.altKey && e.keyCode == 74) ||
181 (e.shiftKey && e.altKey && e.keyCode == 75)) {
182 return;
183 }
184 selectWidgetByID(widget);
185};
186
187
188var initializeGlobalKeyBindings = function(fields) {
189
190 Y.get('document').on("keyup", function(e) {
191 // Shift+Alt+s - Save form
192 if (e.shiftKey && e.altKey && e.keyCode == 83) {
193 Y.one('#save_and_continue_button').invoke('click');
194 }
195 // Shift+Alt+f - Go to search field
196 if (e.shiftKey && e.altKey && e.keyCode == 70) {
197 self.setFocus('search_box');
198 }
199 // Shift+Alt+b - Go to first translation field
200 if (e.shiftKey && e.altKey && e.keyCode == 66) {
201 self.setFocus(fields[0]);
202 }
203 // Shift+Alt+n - Go to next page in batch
204 if (e.shiftKey && e.altKey && e.keyCode == 78) {
205 if (link = Y.one('#batchnav_next')){
206 window.location.assign(link.get('href'))
207 }
208 }
209 // Shift+Alt+p - Go to previous page in batch
210 if (e.shiftKey && e.altKey && e.keyCode == 80) {
211 if (link = Y.one('#batchnav_previous')){
212 window.location.assign(link.get('href'))
213 }
214 }
215 // Shift+Alt+a - Go to first page in batch
216 if (e.shiftKey && e.altKey && e.keyCode == 65) {
217 if (link = Y.one('#batchnav_first')){
218 window.location.assign(link.get('href'))
219 }
220 }
221 // Shift+Alt+l - Go to last page in batch
222 if (e.shiftKey && e.altKey && e.keyCode == 76) {
223 if (link = Y.one('#batchnav_last')){
224 window.location.assign(link.get('href'))
225 }
226 }
227 });
228}
229
230
231var initializeSuggestionsKeyBindings = function(stem) {
232
233 suggestions = Y.all('.' + stem.replace(/_new/,"") + ' input');
234 suggestions.each(function(node) {
235 // Only add keybinding for the first 9 suggestions
236 var index = suggestions.indexOf(node);
237 if (index < 10) {
238 // Shift+Alt+NUMBER - Mark suggestion NUMBER
239 Y.on('key', function(e, id) {
240 selectWidgetByID(id);
241 },
242 '#' + stem, 'down:' + Number(index+49) + '+shift+alt',
243 Y, node.get('id'));
244 }
245 });
246}
247
248
249var initializeFieldsKeyBindings = function (fields) {
250 for (var key = 0; key < fields.length; key++) {
251 var next = key + 1;
252 var previous = key - 1;
253
254 var html_parts = fields[key].split('_');
255 var original_stem = html_parts[0] + '_' + html_parts[1];
256 var translation_stem = fields[key].replace(/_translation_(\d)+_new/,"");
257 var select_widget = (
258 translation_stem + '_' + html_parts[3] + '_' +
259 html_parts[4] + '_new_select');
260
261 Y.on(
262 'change', selectTranslation,
263 '#' + fields[key], Y, select_widget);
264 Y.on(
265 'keypress', selectTranslation,
266 '#' + fields[key], Y, select_widget);
267
268 // Set next field and copy text for all but last field
269 // (last is Save & Continue button)
270 if (key < fields.length - 1) {
271 // Shift+Alt+j - Go to next translation
272 Y.on(
273 'key', setNextFocus, '#' + fields[key],
274 'down:74+shift+alt', Y, fields[next]);
275 // Shift+Alt+KEY_DOWN - Go to next translation
276 Y.on(
277 'key', setNextFocus, '#' + fields[key],
278 'down:40+shift+alt', Y, fields[next]);
279 // Shift+Alt+c - Copy original text
280 Y.on(
281 'key', copyOriginalTextAll, '#' + fields[key],
282 'down:67+shift+alt', Y, original_stem, translation_stem);
283
284 // Shift+Alt+r - Toggle someone should review
285 Y.on(
286 'key',
287 function(e, stem) {
288 toggleWidget(stem + '_force_suggestion');
289 },
290 '#' + fields[key], 'down:82+shift+alt', Y, original_stem);
291
292 // Shift+Alt+d - Toggle dismiss all translations
293 Y.on(
294 'key', function(e, stem) {
295 toggleWidget(stem + '_dismiss');
296 }, '#' + fields[key], 'down:68+shift+alt', Y, original_stem);
297
298 // Shift+Alt+0 - Mark current translation
299 Y.on(
300 'key', function(e, key) {
301 selectWidgetByID(key.replace(/_new/, "_radiobutton"));
302 }, '#' + fields[key], 'down:48+shift+alt', Y, fields[key]);
303
304 initializeSuggestionsKeyBindings(fields[key]);
305 }
306
307 // Set previous field for all but first field
308 if (key > 0) {
309 var parts = fields[previous].split('_');
310 var singular_copy_text = (
311 parts[0] + '_' + parts[1] + '_singular_copy_text');
312 // Shift+Alt+k - Go to previous translation
313 Y.on(
314 'key', setPreviousFocus, '#' + fields[key],
315 'down:75+shift+alt', Y, fields[previous],
316 singular_copy_text);
317 // Shift+Alt+KEY_UP - Go to previous translation
318 Y.on(
319 'key', setPreviousFocus, '#' + fields[key],
320 'down:38+shift+alt', Y, fields[previous],
321 singular_copy_text);
322 }
323 }
324}
325
326
327/**
328 * Initialize event-key bindings such as moving to the next or previous
329 * field, or copying original text
330 */
331self.initializeKeyBindings = function(e) {
332
333 if (translations_order.length < 1) {
334 // If no translations fiels are displayed on the page
335 // don't initialize the translations order
336 return;
337 }
338
339 var fields = translations_order.split(' ');
340 // The last field is Save & Continue button
341 fields.push('save_and_continue_button');
342
343 initializeGlobalKeyBindings(fields);
344 initializeFieldsKeyBindings(fields);
345};
346
347}, "0.1", {"requires": ["event", "event-key", "node", "cookie", "anim"]});
348
45349
=== modified file 'lib/canonical/launchpad/templates/batchnavigator-navigation-links.pt'
--- lib/canonical/launchpad/templates/batchnavigator-navigation-links.pt 2010-01-27 07:38:02 +0000
+++ lib/canonical/launchpad/templates/batchnavigator-navigation-links.pt 2010-02-25 13:03:21 +0000
@@ -29,6 +29,7 @@
29 tal:condition="first_page_url"29 tal:condition="first_page_url"
30 tal:attributes="href first_page_url"30 tal:attributes="href first_page_url"
31 class="first"31 class="first"
32 id="batchnav_first"
32 rel="first"33 rel="first"
33 >First</a>34 >First</a>
34 <span tal:condition="not:first_page_url" class="first inactive"35 <span tal:condition="not:first_page_url" class="first inactive"
@@ -38,6 +39,7 @@
38 tal:condition="prev_page_url"39 tal:condition="prev_page_url"
39 tal:attributes="href prev_page_url"40 tal:attributes="href prev_page_url"
40 class="previous"41 class="previous"
42 id="batchnav_previous"
41 rel="previous"43 rel="previous"
42 >Previous</a>44 >Previous</a>
43 <span tal:condition="not:prev_page_url" class="previous inactive"45 <span tal:condition="not:prev_page_url" class="previous inactive"
@@ -46,6 +48,7 @@
46 <a48 <a
47 tal:condition="next_page_url"49 tal:condition="next_page_url"
48 tal:attributes="href next_page_url"50 tal:attributes="href next_page_url"
51 id="batchnav_next"
49 class="next"52 class="next"
50 rel="next"53 rel="next"
51 ><strong>Next</strong></a>54 ><strong>Next</strong></a>
@@ -57,6 +60,7 @@
57 tal:condition="last_page_url"60 tal:condition="last_page_url"
58 tal:attributes="href last_page_url"61 tal:attributes="href last_page_url"
59 class="last"62 class="last"
63 id="batchnav_last"
60 rel="last"64 rel="last"
61 >Last</a>65 >Last</a>
62 <span tal:condition="not:last_page_url" class="last inactive"66 <span tal:condition="not:last_page_url" class="last inactive"
6367
=== modified file 'lib/lp/translations/browser/pofile.py'
--- lib/lp/translations/browser/pofile.py 2010-01-27 07:38:02 +0000
+++ lib/lp/translations/browser/pofile.py 2010-02-25 13:03:21 +0000
@@ -503,8 +503,6 @@
503503
504 DEFAULT_BATCH_SIZE = 50504 DEFAULT_BATCH_SIZE = 50
505505
506 page_title = "Contributions"
507
508 @property506 @property
509 def _person_name(self):507 def _person_name(self):
510 """Person's display name. Graceful about unknown persons."""508 """Person's display name. Graceful about unknown persons."""
@@ -930,6 +928,26 @@
930 def completeness(self):928 def completeness(self):
931 return '%.0f%%' % self.context.translatedPercentage()929 return '%.0f%%' % self.context.translatedPercentage()
932930
931 def _messages_html_id(self):
932 order = []
933 for message in self.translationmessage_views:
934 if (message.form_is_writeable):
935 for dictionary in message.translation_dictionaries:
936 order.append(
937 dictionary['html_id_translation'] + '_new')
938 return order
939
940 @property
941 def autofocus_html_id(self):
942 if (len(self._messages_html_id()) > 0):
943 return self._messages_html_id()[0]
944 else:
945 return ""
946
947 @property
948 def translations_order(self):
949 return ' '.join(self._messages_html_id())
950
933951
934class POExportView(BaseExportView):952class POExportView(BaseExportView):
935953
936954
=== modified file 'lib/lp/translations/browser/tests/pofile-views.txt'
--- lib/lp/translations/browser/tests/pofile-views.txt 2009-09-17 16:22:32 +0000
+++ lib/lp/translations/browser/tests/pofile-views.txt 2010-02-25 13:03:21 +0000
@@ -145,6 +145,23 @@
145 >>> translationmessage_view.context.translations145 >>> translationmessage_view.context.translations
146 [u'libreta de direcciones de Evolution']146 [u'libreta de direcciones de Evolution']
147147
148To help the JavaScript key navigation the view is exposing the autofocus
149field and a list of all translation fields ordered by the way they are
150listed in the page.
151
152 >>> for translationmessage_view in (
153 ... pofile_view.translationmessage_views):
154 ... translationmessage_view.initialize()
155 >>> print pofile_view.autofocus_html_id
156 msgset_130_es_translation_0_new
157 >>> print pofile_view.translations_order
158 msgset_130_es_translation_0_new msgset_131_es_translation_0_new
159 msgset_132_es_translation_0_new msgset_133_es_translation_0_new
160 msgset_134_es_translation_0_new msgset_135_es_translation_0_new
161 msgset_136_es_translation_0_new msgset_137_es_translation_0_new
162 msgset_138_es_translation_0_new msgset_139_es_translation_0_new
163 msgset_130_es_translation_0_new
164
148It's time to check the submission of translations and the IPOFile statistics165It's time to check the submission of translations and the IPOFile statistics
149update.166update.
150167
@@ -403,7 +420,8 @@
403420
404= POFileNavigation =421= POFileNavigation =
405422
406This class is used to traverse from IPOFile objects to ITranslationMessage ones.423This class is used to traverse from IPOFile objects to ITranslationMessage
424ones.
407425
408 >>> from zope.security.proxy import isinstance426 >>> from zope.security.proxy import isinstance
409 >>> from lp.translations.browser.pofile import POFileNavigation427 >>> from lp.translations.browser.pofile import POFileNavigation
@@ -483,4 +501,4 @@
483And we are redirected to the index page, as expected:501And we are redirected to the index page, as expected:
484502
485 >>> print pofile_view.request.response.getHeader('Location')503 >>> print pofile_view.request.response.getHeader('Location')
486 http://translations.../ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es504 http://trans.../ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es
487505
=== modified file 'lib/lp/translations/browser/translationmessage.py'
--- lib/lp/translations/browser/translationmessage.py 2010-01-27 07:38:02 +0000
+++ lib/lp/translations/browser/translationmessage.py 2010-02-25 13:03:21 +0000
@@ -829,6 +829,30 @@
829 self._redirectToNextPage()829 self._redirectToNextPage()
830 return True830 return True
831831
832 def _messages_html_id(self):
833 order = []
834 message = self.translationmessage_view
835 # If we don't know about plural forms, or there are some other
836 # reason that prevent translations, translationmessage_view is
837 # not created
838 if ((message is not None) and (message.form_is_writeable)):
839 for dictionary in message.translation_dictionaries:
840 order.append(
841 dictionary['html_id_translation'] + '_new')
842 return order
843
844 @property
845 def autofocus_html_id(self):
846 if (len(self._messages_html_id()) > 0):
847 return self._messages_html_id()[0]
848 else:
849 return ""
850
851 @property
852 def translations_order(self):
853 return ' '.join(self._messages_html_id())
854
855
832class CurrentTranslationMessageView(LaunchpadView):856class CurrentTranslationMessageView(LaunchpadView):
833 """Holds all data needed to show an ITranslationMessage.857 """Holds all data needed to show an ITranslationMessage.
834858
835859
=== modified file 'lib/lp/translations/stories/standalone/xx-pofile-translate-newlines-check.txt'
--- lib/lp/translations/stories/standalone/xx-pofile-translate-newlines-check.txt 2010-01-27 07:38:02 +0000
+++ lib/lp/translations/stories/standalone/xx-pofile-translate-newlines-check.txt 2010-02-25 13:03:21 +0000
@@ -43,7 +43,7 @@
43 >>> print browser.url43 >>> print browser.url
44 http://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es/+translate?start=19&batch=144 http://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es/+translate?start=19&batch=1
45 >>> print find_tag_by_id(browser.contents, 'msgset_149_es_translation_0_new')45 >>> print find_tag_by_id(browser.contents, 'msgset_149_es_translation_0_new')
46 <textarea ... name="msgset_149_es_translation_0_new" ...>46 <textarea ... name="msgset_149_es_translation_0_new"...>
4747
48 foo48 foo
4949
@@ -63,7 +63,7 @@
63 >>> print find_tag_by_id(63 >>> print find_tag_by_id(
64 ... browser.contents,64 ... browser.contents,
65 ... 'msgset_149_es_translation_0_new') #doctest: -NORMALIZE_WHITESPACE65 ... 'msgset_149_es_translation_0_new') #doctest: -NORMALIZE_WHITESPACE
66 <textarea ... name="msgset_149_es_translation_0_new" ...>66 <textarea ... name="msgset_149_es_translation_0_new"...>
67 foo</textarea>67 foo</textarea>
6868
69Now, Check that even though the user forgot the trailing new line char,69Now, Check that even though the user forgot the trailing new line char,
7070
=== modified file 'lib/lp/translations/stories/standalone/xx-pofile-translate.txt'
--- lib/lp/translations/stories/standalone/xx-pofile-translate.txt 2010-01-27 07:38:02 +0000
+++ lib/lp/translations/stories/standalone/xx-pofile-translate.txt 2010-02-25 13:03:21 +0000
@@ -30,7 +30,8 @@
3030
31The page is rendered in read-only mode, without any textareas for input.31The page is rendered in read-only mode, without any textareas for input.
3232
33 >>> main_content = find_tag_by_id(browser.contents, 'messages_to_translate')33 >>> main_content = find_tag_by_id(
34 ... browser.contents, 'messages_to_translate')
34 >>> for textarea in main_content.findAll('textarea'):35 >>> for textarea in main_content.findAll('textarea'):
35 ... print 'Found textarea:\n%s' % textarea36 ... print 'Found textarea:\n%s' % textarea
3637
@@ -145,8 +146,8 @@
145146
146 >>> browser = setupBrowser(auth='Basic carlos@canonical.com:test')147 >>> browser = setupBrowser(auth='Basic carlos@canonical.com:test')
147 >>> browser.open("http://translations.launchpad.dev/"148 >>> browser.open("http://translations.launchpad.dev/"
148 ... "ubuntu/hoary/+source/evolution/+pots/evolution-2.2/en_AU/"149 ... "ubuntu/hoary/+source/evolution/+pots/evolution-2.2"
149 ... "+translate?field.alternative_language=es")150 ... "/en_AU/+translate?field.alternative_language=es")
150151
151Elements related 1:1 to a translatable message on this form have names and152Elements related 1:1 to a translatable message on this form have names and
152identifiers constructed as "msgset_<id>," where <id> is the unpadded decimal153identifiers constructed as "msgset_<id>," where <id> is the unpadded decimal
@@ -158,7 +159,7 @@
158 ... print id159 ... print id
159 msgset_130160 msgset_130
160 ...161 ...
161 msgset_130_singular162 msgset_130_singular...
162163
163HTML element identifiers for suggestions and translations on this form are164HTML element identifiers for suggestions and translations on this form are
164constructed as an underscore-separated sequence of:165constructed as an underscore-separated sequence of:
@@ -180,7 +181,9 @@
180 msgset_130_es_suggestion_562_0181 msgset_130_es_suggestion_562_0
181 msgset_130_es_suggestion_562_0_origin182 msgset_130_es_suggestion_562_0_origin
182 msgset_130_es_suggestion_562_0_radiobutton183 msgset_130_es_suggestion_562_0_radiobutton
184 msgset_130_force_suggestion
183 msgset_130_singular185 msgset_130_singular
186 msgset_130_singular_copy_text
184187
185Radio buttons are grouped by their name attribute. The translate page shows188Radio buttons are grouped by their name attribute. The translate page shows
186each translatable message with one radiobutton to select the existing189each translatable message with one radiobutton to select the existing
@@ -190,7 +193,9 @@
190Here we see an example where three suggestions are offered, making for five193Here we see an example where three suggestions are offered, making for five
191identically-named radio buttons and sundry other HTML tags.194identically-named radio buttons and sundry other HTML tags.
192195
193 >>> browser.open('http://translations.launchpad.dev/alsa-utils/trunk/+pots/alsa-utils/es/+translate')196 >>> browser.open(
197 ... 'http://translations.launchpad.dev/alsa-utils/trunk/'
198 ... '+pots/alsa-utils/es/+translate')
194 >>> msgset_198 = get_tags(browser, 'name', 'msgset_198')199 >>> msgset_198 = get_tags(browser, 'name', 'msgset_198')
195 >>> for name in msgset_198:200 >>> for name in msgset_198:
196 ... print name201 ... print name
@@ -213,3 +218,26 @@
213 ... browser.contents, 'msgset_134_es_suggestion_694_0'))218 ... browser.contents, 'msgset_134_es_suggestion_694_0'))
214 tarjetas219 tarjetas
215220
221
222Missing plural forms information
223--------------------------------
224
225If the plural forms are not known for a language, users can not add
226new translations and are asked to help Launchpad Translations by providing
227the plural form informations.
228
229This notice is display when doing batch translations or translating a
230single message.
231
232 >>> browser.open('http://translations.launchpad.dev/ubuntu/hoary/'
233 ... '+source/evolution/+pots/evolution-2.2/ab/+translate')
234 >>> print extract_text(find_tag_by_id(
235 ... browser.contents, 'maincontent'))
236 Launchpad can&#8217;t handle the plural items ...
237
238 >>> browser.open('http://translations.launchpad.dev/ubuntu/hoary/'
239 ... '+source/evolution/+pots/evolution-2.2/ab/5/+translate')
240 >>> print extract_text(find_tag_by_id(
241 ... browser.contents, 'maincontent'))
242 Launchpad can&#8217;t handle the plural items ...
243
216244
=== modified file 'lib/lp/translations/templates/currenttranslationmessage-translate-one.pt'
--- lib/lp/translations/templates/currenttranslationmessage-translate-one.pt 2010-01-27 07:38:02 +0000
+++ lib/lp/translations/templates/currenttranslationmessage-translate-one.pt 2010-02-25 13:03:21 +0000
@@ -42,6 +42,7 @@
42 <a href=""42 <a href=""
43 tal:condition="not:context/potmsgset/is_translation_credit"43 tal:condition="not:context/potmsgset/is_translation_credit"
44 tal:attributes="44 tal:attributes="
45 id string:${view/html_id}_singular_copy_text;
45 onClick string:46 onClick string:
46 javascript:copyInnerHTMLById(47 javascript:copyInnerHTMLById(
47 '${view/html_id}_singular',48 '${view/html_id}_singular',
@@ -172,7 +173,7 @@
172 <tal:translation-dictionaries173 <tal:translation-dictionaries
173 repeat="translation_dictionary view/translation_dictionaries">174 repeat="translation_dictionary view/translation_dictionaries">
174 <tal:plural-form define="plural_index translation_dictionary/plural_index">175 <tal:plural-form define="plural_index translation_dictionary/plural_index">
175 <tr class="secondary translation">176 <tr tal:attributes="class string:secondary translation ${view/html_id}">
176 <th colspan="3">177 <th colspan="3">
177 <label class="language-code">Current178 <label class="language-code">Current
178 <span tal:replace="context/pofile/language/englishname">179 <span tal:replace="context/pofile/language/englishname">
@@ -321,9 +322,10 @@
321 </td>322 </td>
322 </tal:locked>323 </tal:locked>
323 </tr>324 </tr>
324 <tr class="secondary confirm_and_dismiss"325 <tr tal:define="name_id string:${view/html_id}_dismiss"
325 tal:define="name_id string:${view/html_id}_dismiss"326 tal:condition="view/can_confirm_and_dismiss"
326 tal:condition="view/can_confirm_and_dismiss">327 tal:attributes="
328 class string:secondary confirm_and_dismiss ${view/html_id}">
327 <td colspan="4"></td>329 <td colspan="4"></td>
328 <td>330 <td>
329 <label class="fuzzy-checkbox" tal:attributes="for name_id">331 <label class="fuzzy-checkbox" tal:attributes="for name_id">
@@ -504,8 +506,6 @@
504 lang context/pofile/language/dashedcode;506 lang context/pofile/language/dashedcode;
505 dir context/pofile/language/abbreviated_text_dir;507 dir context/pofile/language/abbreviated_text_dir;
506 value translation_dictionary/submitted_translation;508 value translation_dictionary/submitted_translation;
507 onKeyPress string:javascript:selectWidget('${translation_dictionary/html_id_translation}_new_select', event);
508 onChange string:javascript:selectWidget('${translation_dictionary/html_id_translation}_new_select', event);
509 "509 "
510 class="translate expandable"510 class="translate expandable"
511 />511 />
@@ -528,8 +528,6 @@
528 name string:${translation_dictionary/html_id_translation}_new;528 name string:${translation_dictionary/html_id_translation}_new;
529 lang context/pofile/language/dashedcode;529 lang context/pofile/language/dashedcode;
530 dir context/pofile/language/abbreviated_text_dir;530 dir context/pofile/language/abbreviated_text_dir;
531 onKeyPress string:javascript:selectWidget('${translation_dictionary/html_id_translation}_new_select', event);
532 onChange string:javascript:selectWidget('${translation_dictionary/html_id_translation}_new_select', event);
533 ">531 ">
534<tal:content replace="translation_dictionary/submitted_translation" /></textarea>532<tal:content replace="translation_dictionary/submitted_translation" /></textarea>
535 </tal:with-content>533 </tal:with-content>
@@ -548,8 +546,6 @@
548 name string:${translation_dictionary/html_id_translation}_new;546 name string:${translation_dictionary/html_id_translation}_new;
549 lang context/pofile/language/dashedcode;547 lang context/pofile/language/dashedcode;
550 dir context/pofile/language/abbreviated_text_dir;548 dir context/pofile/language/abbreviated_text_dir;
551 onKeyPress string:javascript:selectWidget('${translation_dictionary/html_id_translation}_new_select', event);
552 onChange string:javascript:selectWidget('${translation_dictionary/html_id_translation}_new_select', event);
553 "></textarea>549 "></textarea>
554 </tal:without-content>550 </tal:without-content>
555 </tal:multi-line>551 </tal:multi-line>
@@ -595,7 +591,8 @@
595 value="force_suggestion"591 value="force_suggestion"
596 tal:attributes="592 tal:attributes="
597 checked view/force_suggestion;593 checked view/force_suggestion;
598 name string:${view/html_id}_${language_code}_needsreview"594 name string:${view/html_id}_${language_code}_needsreview;
595 id string:${view/html_id}_force_suggestion;"
599 />596 />
600 <tal:block condition="not:view/is_plural">597 <tal:block condition="not:view/is_plural">
601 Someone should review this translation598 Someone should review this translation
602599
=== modified file 'lib/lp/translations/templates/pofile-translate.pt'
--- lib/lp/translations/templates/pofile-translate.pt 2010-01-27 07:38:02 +0000
+++ lib/lp/translations/templates/pofile-translate.pt 2010-02-25 13:03:21 +0000
@@ -13,6 +13,7 @@
13 color: lightgray;13 color: lightgray;
14 }14 }
15 </style>15 </style>
16
16 <script type="text/javascript"17 <script type="text/javascript"
17 tal:condition="devmode"18 tal:condition="devmode"
18 tal:define="lp_js string:${icingroot}/build"19 tal:define="lp_js string:${icingroot}/build"
@@ -20,50 +21,13 @@
20 <script type="text/javascript">21 <script type="text/javascript">
21 registerLaunchpadFunction(insertAllExpansionButtons);22 registerLaunchpadFunction(insertAllExpansionButtons);
2223
23 LPS.use('node', 'cookie', 'anim', 'lp.pofile', function(Y) {24 LPS.use('lp.pofile', function(Y) {
24
25 var hide_notification = function(node) {
26 var hide_anim = new Y.Anim({
27 node: node,
28 to: { height: 0,
29 marginTop: 0, marginBottom: 0,
30 paddingTop: 0, paddingBottom: 0 }
31 });
32 node.setStyle('border', 'none');
33 hide_anim.set('duration', 0.4);
34 hide_anim.on('end', function(e) {
35 node.setStyle('display', 'none');
36 });
37 hide_anim.run();
38 }
39
40 var updateNotificationBox = function(e) {
41 var notice = Y.one('.important-notice-container');
42 var balloon = notice.one('.important-notice-balloon');
43 var dismiss_notice_cookie = ('translation-docs-for-' +
44 documentation_cookie);
45
46 // Check the cookie to see if the user has already dismissed
47 // the notification box for this session.
48 var already_seen = Y.Cookie.get(dismiss_notice_cookie, Boolean);
49 if (already_seen) {
50 notice.setStyle('display', 'none');
51 }
52
53 var cancel_button = notice.one(
54 '.important-notice-cancel-button');
55 // Cancel button starts out hidden. If user has JavaScript,
56 // then we want to show it.
57 cancel_button.setStyle('visibility', 'visible');
58 cancel_button.on('click', function(e) {
59 e.halt();
60 hide_notification(balloon);
61 Y.Cookie.set(dismiss_notice_cookie, true);
62 });
63 };
64
65 Y.on('domready', Y.lp.pofile.setupSuggestionDismissal);25 Y.on('domready', Y.lp.pofile.setupSuggestionDismissal);
66 Y.on('domready', updateNotificationBox);26 Y.on('domready', Y.lp.pofile.updateNotificationBox);
27 Y.on('domready', Y.lp.pofile.initializeKeyBindings);
28 Y.on('domready', function(e) {
29 Y.lp.pofile.setFocus(autofocus_field);
30 });
67 });31 });
68 </script>32 </script>
69 </div>33 </div>
@@ -262,6 +226,7 @@
262 <td style="text-align: right;">226 <td style="text-align: right;">
263 <input type="submit"227 <input type="submit"
264 name="submit_translations"228 name="submit_translations"
229 id="save_and_continue_button"
265 value="Save &amp; Continue"230 value="Save &amp; Continue"
266 />231 />
267 </td>232 </td>
@@ -277,6 +242,8 @@
277 <tal:status replace="structure context/@@+access" />242 <tal:status replace="structure context/@@+access" />
278 <tal:contributors replace="structure context/@@+contributors" />243 <tal:contributors replace="structure context/@@+contributors" />
279 </tal:havepluralforms>244 </tal:havepluralforms>
245 <metal:pofile-js-footer
246 use-macro="context/@@+translations-macros/pofile-js-footer" />
280 </div>247 </div>
281 </body>248 </body>
282</html>249</html>
283250
=== modified file 'lib/lp/translations/templates/translationmessage-translate.pt'
--- lib/lp/translations/templates/translationmessage-translate.pt 2010-01-27 07:38:02 +0000
+++ lib/lp/translations/templates/translationmessage-translate.pt 2010-02-25 13:03:21 +0000
@@ -20,6 +20,10 @@
20 <script type="text/javascript">20 <script type="text/javascript">
21 LPS.use('node', 'lp.pofile', function(Y) {21 LPS.use('node', 'lp.pofile', function(Y) {
22 Y.on('domready', Y.lp.pofile.setupSuggestionDismissal);22 Y.on('domready', Y.lp.pofile.setupSuggestionDismissal);
23 Y.on('domready', Y.lp.pofile.initializeKeyBindings);
24 Y.on('domready', function(e) {
25 Y.lp.pofile.setFocus(autofocus_field);
26 });
23 });27 });
24 </script>28 </script>
25 </div>29 </div>
@@ -76,6 +80,7 @@
76 <th colspan="5" style="text-align: right;">80 <th colspan="5" style="text-align: right;">
77 <input type="submit"81 <input type="submit"
78 name="submit_translations"82 name="submit_translations"
83 id="save_and_continue_button"
79 value="Save &amp; Continue" />84 value="Save &amp; Continue" />
80 </th>85 </th>
81 </tr>86 </tr>
@@ -89,6 +94,8 @@
89 replace="structure view/batchnav/@@+navigation-links-lower" />94 replace="structure view/batchnav/@@+navigation-links-lower" />
90 <tal:status replace="structure context/pofile/@@+access" />95 <tal:status replace="structure context/pofile/@@+access" />
91 </tal:havepluralforms>96 </tal:havepluralforms>
97 <metal:pofile-js-footer
98 use-macro="context/@@+translations-macros/pofile-js-footer" />
92 </div>99 </div>
93100
94 </body>101 </body>
95102
=== modified file 'lib/lp/translations/templates/translations-macros.pt'
--- lib/lp/translations/templates/translations-macros.pt 2010-01-27 07:38:02 +0000
+++ lib/lp/translations/templates/translations-macros.pt 2010-02-25 13:03:21 +0000
@@ -6,7 +6,7 @@
6<metal:render-suggestion define-macro="render-suggestion">6<metal:render-suggestion define-macro="render-suggestion">
7<tal:submission condition="submission">7<tal:submission condition="submission">
8 <tal:not-empty condition="not:submission/is_empty">8 <tal:not-empty condition="not:submission/is_empty">
9 <tr tal:attributes="class string:secondary ${dismissable};9 <tr tal:attributes="class string:secondary ${dismissable} ${submission/translation_html_id};
10 id submission/row_html_id">10 id submission/row_html_id">
11 <th colspan="3" tal:content="section_title">11 <th colspan="3" tal:content="section_title">
12 Packaged:12 Packaged:
@@ -130,6 +130,17 @@
130</metal:nav-pofile-subpages>130</metal:nav-pofile-subpages>
131131
132132
133<metal:pofile-js-footer define-macro="pofile-js-footer">
134 <script type="text/javascript"
135 tal:content="
136 structure string:<!--
137 var autofocus_field = '${view/autofocus_html_id}';
138 var translations_order = '${view/translations_order}';
139 var plural_forms = ${context/plural_forms};
140 // -->" />
141</metal:pofile-js-footer>
142
143
133<metal:translations-js define-macro="translations-js">144<metal:translations-js define-macro="translations-js">
134 <script145 <script
135 type="text/javascript"146 type="text/javascript"