modinfo in the translate toolkit is now a 2-tuple containing the mtime and
[pootle.git] / translatepage.py
blobe72b002ef60d2560bd460b1d36c8da826313b1b0
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # Copyright 2004-2006 Zuza Software Foundation
5 #
6 # This file is part of translate.
8 # translate is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
13 # translate is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with translate; if not, write to the Free Software
20 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22 import re
23 from jToolkit import spellcheck
24 from Pootle import pagelayout
25 from Pootle import projects
26 from Pootle import pootlefile
27 from translate.storage import po
28 from translate.misc.multistring import multistring
29 import difflib
30 import urllib
32 xml_re = re.compile("<.*?>")
34 def oddoreven(polarity):
35 if polarity % 2 == 0:
36 return "even"
37 elif polarity % 2 == 1:
38 return "odd"
40 class TranslatePage(pagelayout.PootleNavPage):
41 """the page which lets people edit translations"""
42 def __init__(self, project, session, argdict, dirfilter=None):
43 self.argdict = argdict
44 self.dirfilter = dirfilter
45 self.project = project
46 self.altproject = None
47 # do we have enabled alternative source language?
48 self.enablealtsrc = getattr(session.instance, "enablealtsrc", "False")
49 if self.enablealtsrc == 'True':
50 # try to get the project if the user has chosen an alternate source language
51 altsrc = session.getaltsrclanguage()
52 if altsrc != '':
53 try:
54 self.altproject = self.project.potree.getproject(altsrc, self.project.projectcode)
55 except IndexError:
56 pass
57 self.matchnames = self.getmatchnames(self.project.checker)
58 self.searchtext = self.argdict.get("searchtext", "")
59 # TODO: fix this in jToolkit
60 if isinstance(self.searchtext, str):
61 self.searchtext = self.searchtext.decode("utf8")
62 self.showassigns = self.argdict.get("showassigns", 0)
63 if isinstance(self.showassigns, (str, unicode)) and self.showassigns.isdigit():
64 self.showassigns = int(self.showassigns)
65 self.session = session
66 self.localize = session.localize
67 self.rights = self.project.getrights(self.session)
68 self.instance = session.instance
69 self.lastitem = None
70 self.pofilename = self.argdict.pop("pofilename", None)
71 if self.pofilename == "":
72 self.pofilename = None
73 if self.pofilename is None and self.dirfilter is not None and \
74 (self.dirfilter.endswith(".po") or self.dirfilter.endswith(".xlf")):
75 self.pofilename = self.dirfilter
76 self.receivetranslations()
77 # TODO: clean up modes to be one variable
78 self.viewmode = self.argdict.get("view", 0) and "view" in self.rights
79 self.reviewmode = self.argdict.get("review", 0)
80 self.translatemode = self.argdict.get("translate", 0) or self.argdict.get("searchtext", 0) and ("translate" in self.rights or "suggest" in self.rights)
81 notice = {}
82 try:
83 self.finditem()
84 except StopIteration, stoppedby:
85 notice = self.getfinishedtext(stoppedby)
86 self.item = None
87 items = self.maketable()
88 # self.pofilename can change in search...
89 givenpofilename = self.pofilename
90 formaction = self.makelink("")
91 mainstats = ""
92 pagelinks = None
93 if self.viewmode:
94 rows = self.getdisplayrows("view")
95 icon="file"
96 else:
97 rows = self.getdisplayrows("translate")
98 icon="edit"
99 if self.pofilename is not None:
100 postats = self.project.getpostats(self.pofilename)
101 untranslated, fuzzy = postats["untranslated"], postats["fuzzy"]
102 translated, total = postats["translated"], postats["total"]
103 mainstats = self.localize("%d/%d translated\n(%d untranslated, %d fuzzy)", len(translated), len(total), len(untranslated), len(fuzzy))
104 pagelinks = self.getpagelinks("?translate=1&view=1", rows)
105 navbarpath_dict = self.makenavbarpath_dict(self.project, self.session, self.pofilename, dirfilter=self.dirfilter or "")
106 # templatising
107 templatename = "translatepage"
108 instancetitle = getattr(session.instance, "title", session.localize("Pootle Demo"))
109 # l10n: first parameter: name of the installation (like "Pootle")
110 # l10n: second parameter: project name
111 # l10n: third parameter: target language
112 # l10n: fourth parameter: file name
113 pagetitle = self.localize("%s: translating %s into %s: %s", instancetitle, self.project.projectname, self.project.languagename, self.pofilename)
114 language = {"code": pagelayout.weblanguage(self.project.languagecode), "name": self.project.languagename, "dir": pagelayout.languagedir(self.project.languagecode)}
115 sessionvars = {"status": session.status, "isopen": session.isopen, "issiteadmin": session.issiteadmin()}
116 stats = {"summary": mainstats, "checks": [], "tracks": [], "assigns": []}
117 templatevars = {"pagetitle": pagetitle,
118 "project": {"code": self.project.projectcode, "name": self.project.projectname},
119 "language": language,
120 "pofilename": self.pofilename,
121 # navigation bar
122 "navitems": [{"icon": icon, "path": navbarpath_dict, "actions": {}, "stats": stats}],
123 "pagelinks": pagelinks,
124 # translation form
125 "actionurl": formaction,
126 "notice": notice,
127 # l10n: Heading above the table column with the source language
128 "original_title": self.localize("Original"),
129 # l10n: Heading above the table column with the target language
130 "translation_title": self.localize("Translation"),
131 "items": items,
132 "reviewmode": self.reviewmode,
133 "accept_button": self.localize("Accept"),
134 "reject_button": self.localize("Reject"),
135 "fuzzytext": self.localize("Fuzzy"),
136 # l10n: Heading above the textarea for translator comments.
137 "translator_comments_title": self.localize("Translator comments"),
138 # l10n: Heading above the comments extracted from the programing source code
139 "developer_comments_title": self.localize("Developer comments"),
140 # l10n: This heading refers to related translations and terminology
141 "related_title": self.localize("Related"),
142 # optional sections, will appear if these values are replaced
143 "assign": None,
144 # l10n: text next to search field
145 "search": {"title": self.localize("Search")},
146 # hidden widgets
147 "searchtext": self.searchtext,
148 "pofilename": givenpofilename,
149 # general vars
150 "session": sessionvars,
151 "instancetitle": instancetitle}
153 if self.showassigns and "assign" in self.rights:
154 templatevars["assign"] = self.getassignbox()
155 pagelayout.PootleNavPage.__init__(self, templatename, templatevars, session, bannerheight=81)
156 self.addfilelinks()
158 def getfinishedtext(self, stoppedby):
159 """gets notice to display when the translation is finished"""
160 # l10n: "batch" refers to the set of translations that were reviewed
161 title = self.localize("End of batch")
162 finishedlink = "index.html?" + "&".join(["%s=%s" % (arg, value) for arg, value in self.argdict.iteritems() if arg.startswith("show") or arg == "editing"])
163 returnlink = self.localize("Click here to return to the index")
164 stoppedbytext = stoppedby.args[0]
165 return {"title": title, "stoppedby": stoppedbytext, "finishedlink": finishedlink, "returnlink": returnlink}
167 def getpagelinks(self, baselink, pagesize):
168 """gets links to other pages of items, based on the given baselink"""
169 baselink += "&pofilename=%s" % self.pofilename
170 pagelinks = []
171 pofilelen = self.project.getpofilelen(self.pofilename)
172 if pofilelen <= pagesize or self.firstitem is None:
173 return pagelinks
174 lastitem = min(pofilelen-1, self.firstitem + pagesize - 1)
175 if pofilelen > pagesize and not self.firstitem == 0:
176 # l10n: noun (the start)
177 pagelinks.append({"href": baselink + "&item=0", "text": self.localize("Start")})
178 else:
179 # l10n: noun (the start)
180 pagelinks.append({"text": self.localize("Start")})
181 if self.firstitem > 0:
182 linkitem = max(self.firstitem - pagesize, 0)
183 # l10n: the parameter refers to the number of messages
184 pagelinks.append({"href": baselink + "&item=%d" % linkitem, "text": self.localize("Previous %d", (self.firstitem - linkitem))})
185 else:
186 # l10n: the parameter refers to the number of messages
187 pagelinks.append({"text": self.localize("Previous %d", pagesize)})
188 # l10n: the third parameter refers to the total number of messages in the file
189 pagelinks.append({"text": self.localize("Items %d to %d of %d", self.firstitem+1, lastitem+1, pofilelen)})
190 if self.firstitem + len(self.translations) < self.project.getpofilelen(self.pofilename):
191 linkitem = self.firstitem + pagesize
192 itemcount = min(pofilelen - linkitem, pagesize)
193 # l10n: the parameter refers to the number of messages
194 pagelinks.append({"href": baselink + "&item=%d" % linkitem, "text": self.localize("Next %d", itemcount)})
195 else:
196 # l10n: the parameter refers to the number of messages
197 pagelinks.append({"text": self.localize("Next %d", pagesize)})
198 if pofilelen > pagesize and (self.item + pagesize) < pofilelen:
199 # l10n: noun (the end)
200 pagelinks.append({"href": baselink + "&item=%d" % max(pofilelen - pagesize, 0), "text": self.localize("End")})
201 else:
202 # l10n: noun (the end)
203 pagelinks.append({"text": self.localize("End")})
204 for n, pagelink in enumerate(pagelinks):
205 if n < len(pagelinks)-1:
206 pagelink["sep"] = " | "
207 else:
208 pagelink["sep"] = ""
209 return pagelinks
211 def addfilelinks(self):
212 """adds a section on the current file, including any checks happening"""
213 if self.showassigns and "assign" in self.rights:
214 self.templatevars["assigns"] = self.getassignbox()
215 if self.pofilename is not None:
216 if self.matchnames:
217 checknames = [matchname.replace("check-", "", 1) for matchname in self.matchnames]
218 # TODO: put the following parameter in quotes, since it will be foreign in all target languages
219 # l10n: the parameter is the name of one of the quality checks, like "fuzzy"
220 self.templatevars["checking_text"] = self.localize("checking %s", ", ".join(checknames))
222 def getassignbox(self):
223 """gets strings if the user can assign strings"""
224 users = [username for username, userprefs in self.session.loginchecker.users.iteritems() if username != "__dummy__"]
225 users.sort()
226 return {
227 "title": self.localize("Assign Strings"),
228 "user_title": self.localize("Assign to User"),
229 "action_title": self.localize("Assign Action"),
230 "submit_text": self.localize("Assign Strings"),
231 "users": users,
234 def receivetranslations(self):
235 """receive any translations submitted by the user"""
236 if self.pofilename is None:
237 return
238 backs = []
239 skips = []
240 submitsuggests = []
241 submits = []
242 accepts = []
243 rejects = []
244 translations = {}
245 suggestions = {}
246 comments = {}
247 fuzzies = {}
248 keymatcher = re.compile("(\D+)([0-9.]+)")
249 def parsekey(key):
250 match = keymatcher.match(key)
251 if match:
252 keytype, itemcode = match.groups()
253 return keytype, itemcode
254 return None, None
255 def pointsplit(item):
256 dotcount = item.count(".")
257 if dotcount == 2:
258 item, pointitem, subpointitem = item.split(".", 2)
259 return int(item), int(pointitem), int(subpointitem)
260 elif dotcount == 1:
261 item, pointitem = item.split(".", 1)
262 return int(item), int(pointitem), None
263 else:
264 return int(item), None, None
265 delkeys = []
266 for key, value in self.argdict.iteritems():
267 keytype, item = parsekey(key)
268 if keytype is None:
269 continue
270 item, pointitem, subpointitem = pointsplit(item)
271 if keytype == "skip":
272 skips.append(item)
273 elif keytype == "back":
274 backs.append(item)
275 elif keytype == "submitsuggest":
276 submitsuggests.append(item)
277 elif keytype == "submit":
278 submits.append(item)
279 elif keytype == "accept":
280 accepts.append((item, pointitem))
281 elif keytype == "reject":
282 rejects.append((item, pointitem))
283 elif keytype == "translator_comments":
284 # We need to remove carriage returns from the input.
285 value = value.replace("\r", "")
286 comments[item] = value
287 elif keytype == "fuzzy":
288 fuzzies[item] = value
289 elif keytype == "trans":
290 value = self.unescapesubmition(value)
291 if pointitem is not None:
292 translations.setdefault(item, {})[pointitem] = value
293 else:
294 translations[item] = value
295 elif keytype == "suggest":
296 suggestions.setdefault((item, pointitem), {})[subpointitem] = value
297 elif keytype == "orig-pure":
298 # this is just to remove the hidden fields from the argdict
299 pass
300 else:
301 continue
302 delkeys.append(key)
305 for key in delkeys:
306 del self.argdict[key]
307 for item in skips:
308 self.lastitem = item
309 for item in backs:
310 self.lastitem = item - 2
311 for item in submitsuggests:
312 if item in skips or item not in translations:
313 continue
314 value = translations[item]
315 self.project.suggesttranslation(self.pofilename, item, value, self.session)
316 self.lastitem = item
318 for item in submits:
319 if item in skips or item not in translations:
320 continue
322 newvalues = {}
323 newvalues["target"] = translations[item]
324 if isinstance(newvalues["target"], dict) and len(newvalues["target"]) == 1 and 0 in newvalues["target"]:
325 newvalues["target"] = newvalues["target"][0]
327 newvalues["fuzzy"] = False
328 if (fuzzies.get(item) == u'on'):
329 newvalues["fuzzy"] = True
331 translator_comments = comments.get(item)
332 if translator_comments:
333 newvalues["translator_comments"] = translator_comments
335 self.project.updatetranslation(self.pofilename, item, newvalues, self.session)
337 self.lastitem = item
338 for item, suggid in rejects:
339 value = suggestions[item, suggid]
340 if isinstance(value, dict) and len(value) == 1 and 0 in value:
341 value = value[0]
342 self.project.rejectsuggestion(self.pofilename, item, suggid, value, self.session)
343 self.lastitem = item
344 for item, suggid in accepts:
345 if (item, suggid) in rejects or (item, suggid) not in suggestions:
346 continue
347 value = suggestions[item, suggid]
348 if isinstance(value, dict) and len(value) == 1 and 0 in value:
349 value = value[0]
350 self.project.acceptsuggestion(self.pofilename, item, suggid, value, self.session)
351 self.lastitem = item
353 def getmatchnames(self, checker):
354 """returns any checker filters the user has asked to match..."""
355 matchnames = []
356 for checkname in self.argdict:
357 if checkname in ["fuzzy", "untranslated", "translated"]:
358 matchnames.append(checkname)
359 elif checkname in checker.getfilters():
360 matchnames.append("check-" + checkname)
361 matchnames.sort()
362 return matchnames
364 def getusernode(self):
365 """gets the user's prefs node"""
366 if self.session.isopen:
367 return getattr(self.session.loginchecker.users, self.session.username.encode("utf-8"), None)
368 else:
369 return None
371 def finditem(self):
372 """finds the focussed item for this page, searching as neccessary"""
373 item = self.argdict.pop("item", None)
374 if item is None:
375 try:
376 search = pootlefile.Search(dirfilter=self.dirfilter, matchnames=self.matchnames, searchtext=self.searchtext)
377 # TODO: find a nicer way to let people search stuff assigned to them (does it by default now)
378 # search.assignedto = self.argdict.get("assignedto", self.session.username)
379 search.assignedto = self.argdict.get("assignedto", None)
380 search.assignedaction = self.argdict.get("assignedaction", None)
381 self.pofilename, self.item = self.project.searchpoitems(self.pofilename, self.lastitem, search).next()
382 except StopIteration:
383 if self.lastitem is None:
384 raise StopIteration(self.localize("There are no items matching that search ('%s')", self.searchtext))
385 else:
386 raise StopIteration(self.localize("You have finished going through the items you selected"))
387 else:
388 if not item.isdigit():
389 raise ValueError("Invalid item given")
390 self.item = int(item)
391 if self.pofilename is None:
392 raise ValueError("Received item argument but no pofilename argument")
393 self.project.track(self.pofilename, self.item, "being edited by %s" % self.session.username)
395 def getdisplayrows(self, mode):
396 """get the number of rows to display for the given mode"""
397 if mode == "view":
398 prefsfield = "viewrows"
399 default = 10
400 maximum = 100
401 elif mode == "translate":
402 prefsfield = "translaterows"
403 default = 7
404 maximum = 20
405 else:
406 raise ValueError("getdisplayrows has no mode '%s'" % mode)
407 usernode = self.getusernode()
408 rowsdesired = getattr(usernode, prefsfield, default)
409 if isinstance(rowsdesired, basestring):
410 if rowsdesired == "":
411 rowsdesired = default
412 else:
413 rowsdesired = int(rowsdesired)
414 rowsdesired = min(rowsdesired, maximum)
415 return rowsdesired
417 def gettranslations(self):
418 """gets the list of translations desired for the view, and sets editable and firstitem parameters"""
419 if self.item is None:
420 self.editable = []
421 self.firstitem = self.item
422 return []
423 elif self.viewmode:
424 self.editable = []
425 self.firstitem = self.item
426 rows = self.getdisplayrows("view")
427 return self.project.getitems(self.pofilename, self.item, self.item+rows)
428 else:
429 self.editable = [self.item]
430 rows = self.getdisplayrows("translate")
431 before = rows / 2
432 fromitem = self.item - before
433 self.firstitem = max(self.item - before, 0)
434 toitem = self.firstitem + rows
435 return self.project.getitems(self.pofilename, fromitem, toitem)
437 def maketable(self):
438 self.translations = self.gettranslations()
439 items = []
440 if self.reviewmode and self.item is not None:
441 suggestions = {self.item: self.project.getsuggestions(self.pofilename, self.item)}
442 for row, unit in enumerate(self.translations):
443 tmsuggestions = []
444 if isinstance(unit.source, multistring):
445 orig = unit.source.strings
446 else:
447 orig = [unit.source]
448 if isinstance(unit.target, multistring):
449 trans = unit.target.strings
450 else:
451 trans = [unit.target]
452 nplurals, plurals = self.project.getpofile(self.pofilename).getheaderplural()
453 try:
454 if len(orig) > 1:
455 if not (nplurals and nplurals.isdigit()):
456 # The file doesn't have plural information declared. Let's get it from
457 # the language
458 nplurals = getattr(getattr(self.session.instance.languages, self.project.languagecode, None), "nplurals", "")
459 nplurals = int(nplurals)
460 if len(trans) != nplurals:
461 # Chop if in case it is too long
462 trans = trans[:nplurals]
463 trans.extend([u""]* (nplurals-len(trans)))
464 except Exception:
465 # Something went wrong, lets just give nothing
466 trans = []
467 item = self.firstitem + row
468 origdict = self.getorigdict(item, orig, item in self.editable)
469 transmerge = {}
471 message_context = ""
472 if item in self.editable:
473 translator_comments = unit.getnotes(origin="translator")
474 developer_comments = self.escapetext(unit.getnotes(origin="developer"), stripescapes=True)
475 locations = " ".join(unit.getlocations())
476 if isinstance(unit, po.pounit):
477 message_context = "".join(unit.getcontext())
478 tmsuggestions = self.project.gettmsuggestions(self.pofilename, self.item)
479 tmsuggestions.extend(self.project.getterminology(self.session, self.pofilename, self.item))
481 if self.reviewmode:
482 translator_comments = self.escapetext(unit.getnotes(origin="translator"), stripescapes=True)
483 itemsuggestions = [suggestion.target.strings for suggestion in suggestions[item]]
484 transmerge = self.gettransreview(item, trans, itemsuggestions)
485 else:
486 transmerge = self.gettransedit(item, trans)
487 else:
488 translator_comments = unit.getnotes(origin="translator")
489 developer_comments = unit.getnotes(origin="developer")
490 locations = ""
491 transmerge = self.gettransview(item, trans)
492 transdict = {"itemid": "trans%d" % item,
493 "focus_class": origdict["focus_class"],
494 "isplural": len(trans) > 1,
496 transdict.update(transmerge)
497 polarity = oddoreven(item)
498 if item in self.editable:
499 focus_class = "translate-focus"
500 else:
501 focus_class = ""
503 state_class = ""
504 fuzzy = None
505 if unit.isfuzzy():
506 state_class += "translate-translation-fuzzy"
507 fuzzy = "checked"
508 itemdict = {
509 "itemid": item,
510 "orig": origdict,
511 "trans": transdict,
512 "polarity": polarity,
513 "focus_class": focus_class,
514 "editable": item in self.editable,
515 "state_class": state_class,
516 "fuzzy": fuzzy,
517 "translator_comments": translator_comments,
518 "developer_comments": developer_comments,
519 "locations": locations,
520 "message_context": message_context,
521 "tm": tmsuggestions,
524 altsrcdict = {"available": False}
525 # do we have enabled alternative source language?
526 if self.enablealtsrc == 'True':
527 # get alternate source project information in a dictionary
528 if item in self.editable:
529 altsrcdict = self.getaltsrcdict(origdict)
530 itemdict["altsrc"] = altsrcdict
532 items.append(itemdict)
533 return items
535 def fancyspaces(self, string):
536 """Returns the fancy spaces that are easily visible."""
537 spaces = string.group()
538 while spaces[0] in "\t\n\r":
539 spaces = spaces[1:]
540 return '<span class="translation-space"> </span>\n' * len(spaces)
542 def escapetext(self, text, fancyspaces=True, stripescapes=False):
543 """Replace special characters &, <, >, add and handle escapes if asked."""
544 text = text.replace("&", "&amp;") # Must be done first!
545 text = text.replace("<", "&lt;").replace(">", "&gt;")
547 if stripescapes:
548 text = text.replace("\n", '<br />')
549 text = text.replace("\r", '<br />')
550 else:
551 fancyescape = lambda escape: \
552 '<span class="translation-highlight-escape">%s</span>' % escape
553 fancy_xml = lambda escape: \
554 '<span class="translation-highlight-html">%s</span>' % escape.group()
555 text = xml_re.sub(fancy_xml, text)
557 text = text.replace("\r\n", fancyescape('\\r\\n') + '<br />')
558 text = text.replace("\n", fancyescape('\\n') + '<br />')
559 text = text.replace("\r", fancyescape('\\r') + '<br />')
560 text = text.replace("\t", fancyescape('\\t'))
561 text = text.replace("<br />", '<br />\n')
562 # we don't need it at the end of the string
563 if text.endswith("<br />\n"):
564 text = text[:-len("<br />\n")]
566 if fancyspaces:
567 text = self.addfancyspaces(text)
568 return text
570 def addfancyspaces(self, text):
571 """Insert fancy spaces"""
572 #More than two consecutive:
573 text = re.sub("[ ]{2,}", self.fancyspaces, text)
574 #At start of string
575 text = re.sub("^[ ]+", self.fancyspaces, text)
576 #After newline
577 text = re.sub("\\n([ ]+)", self.fancyspaces, text)
578 #At end of string
579 text = re.sub("[ ]+$", self.fancyspaces, text)
580 return text
582 def escapefortextarea(self, text):
583 text = text.replace("&", "&amp;") # Must be done first!
584 text = text.replace("<", "&lt;").replace(">", "&gt;")
585 text = text.replace("\r\n", '\\r\\n')
586 text = text.replace("\n", '\\n')
587 text = text.replace("\\n", '\\n\n')
588 text = text.replace("\t", '\\t')
589 return text
591 def unescapesubmition(self, text):
592 text = text.replace("\t", "")
593 text = text.replace("\n", "")
594 text = text.replace("\r", "")
595 text = text.replace("\\t", "\t")
596 text = text.replace("\\n", "\n")
597 text = text.replace("\\r", "\r")
598 return text
600 def getorigdict(self, item, orig, editable):
601 if editable:
602 focus_class = "translate-original-focus"
603 else:
604 focus_class = "autoexpand"
605 purefields = []
606 for pluralid, pluraltext in enumerate(orig):
607 pureid = "orig-pure%d.%d" % (item, pluralid)
608 purefields.append({"pureid": pureid, "name": pureid, "value": pluraltext})
609 origdict = {
610 "focus_class": focus_class,
611 "itemid": "orig%d" % item,
612 "pure": purefields,
613 "isplural": len(orig) > 1 or None,
614 "singular_title": self.localize("Singular"),
615 "plural_title": self.localize("Plural"),
617 if len(orig) > 1:
618 origdict["singular_text"] = self.escapetext(orig[0])
619 origdict["plural_text"] = self.escapetext(orig[1])
620 else:
621 origdict["text"] = self.escapetext(orig[0])
622 return origdict
624 def geteditlink(self, item):
625 """gets a link to edit the given item, if the user has permission"""
626 if "translate" in self.rights or "suggest" in self.rights:
627 translateurl = "?translate=1&item=%d&pofilename=%s" % (item, urllib.quote(self.pofilename, '/'))
628 # l10n: verb
629 return {"href": translateurl, "text": self.localize("Edit"), "linkid": "editlink%d" % item}
630 else:
631 return {}
633 def gettransbuttons(self, item, desiredbuttons):
634 """gets buttons for actions on translation"""
635 if "suggest" in desiredbuttons and "suggest" not in self.rights:
636 desiredbuttons.remove("suggest")
637 if "translate" in desiredbuttons and "translate" not in self.rights:
638 desiredbuttons.remove("translate")
639 specialchars = getattr(getattr(self.session.instance.languages, self.project.languagecode, None), "specialchars", "")
640 if isinstance(specialchars, str):
641 specialchars = specialchars.decode("utf-8")
642 return {"desired": desiredbuttons,
643 "item": item,
644 # l10n: verb
645 "copy_text": self.localize("Copy"),
646 "skip": self.localize("Skip"),
647 # l10n: verb
648 "back": self.localize("Back"),
649 "suggest": self.localize("Suggest"),
650 "submit": self.localize("Submit"),
651 "specialchars": specialchars,
652 # l10n: action that increases the height of the textarea
653 "grow": self.localize("Grow"),
654 # l10n: action that decreases the height of the textarea
655 "shrink": self.localize("Shrink"),
656 # l10n: action that increases the width of the textarea
659 def gettransedit(self, item, trans):
660 """returns a widget for editing the given item and translation"""
661 transdict = {
662 "rows": 5,
663 "cols": 40,
665 if "translate" in self.rights or "suggest" in self.rights:
666 usernode = self.getusernode()
667 transdict = {
668 "rows": getattr(usernode, "inputheight", 5),
669 "cols": getattr(usernode, "inputwidth", 40),
671 focusbox = ""
672 spellargs = {"standby_url": "spellingstandby.html", "js_url": "/js/spellui.js", "target_url": "spellcheck.html"}
673 if len(trans) > 1:
674 buttons = self.gettransbuttons(item, ["back", "skip", "copy", "suggest", "translate"])
675 forms = []
676 for pluralitem, pluraltext in enumerate(trans):
677 pluralform = self.localize("Plural Form %d", pluralitem)
678 pluraltext = self.escapefortextarea(pluraltext)
679 textid = "trans%d.%d" % (item, pluralitem)
680 forms.append({"title": pluralform, "name": textid, "text": pluraltext, "n": pluralitem})
681 if not focusbox:
682 focusbox = textid
683 transdict["forms"] = forms
684 elif trans:
685 buttons = self.gettransbuttons(item, ["back", "skip", "copy", "suggest", "translate", "resize"])
686 transdict["text"] = self.escapefortextarea(trans[0])
687 textid = "trans%d" % item
688 focusbox = textid
689 else:
690 # Perhaps there is no plural information available
691 buttons = self.gettransbuttons(item, ["back", "skip"])
692 # l10n: This is an error message that will display if the relevant problem occurs
693 transdict["text"] = self.escapefortextarea(self.localize("Translation not possible because plural information for your language is not available. Please contact the site administrator."))
694 textid = "trans%d" % item
695 focusbox = textid
697 transdict["can_spell"] = spellcheck.can_check_lang(self.project.languagecode)
698 transdict["spell_args"] = spellargs
699 transdict["buttons"] = buttons
700 transdict["focusbox"] = focusbox
701 else:
702 # TODO: work out how to handle this (move it up?)
703 transdict.update(self.gettransview(item, trans, textarea=True))
704 buttons = self.gettransbuttons(item, ["back", "skip"])
705 transdict["buttons"] = buttons
706 return transdict
708 def highlightdiffs(self, text, diffs, issrc=True):
709 """highlights the differences in diffs in the text.
710 diffs should be list of diff opcodes
711 issrc specifies whether to use the src or destination positions in reconstructing the text
712 this escapes the text on the fly to prevent confusion in escaping the highlighting"""
713 if issrc:
714 diffstart = [(i1, 'start', tag) for (tag, i1, i2, j1, j2) in diffs if tag != 'equal']
715 diffstop = [(i2, 'stop', tag) for (tag, i1, i2, j1, j2) in diffs if tag != 'equal']
716 else:
717 diffstart = [(j1, 'start', tag) for (tag, i1, i2, j1, j2) in diffs if tag != 'equal']
718 diffstop = [(j2, 'stop', tag) for (tag, i1, i2, j1, j2) in diffs if tag != 'equal']
719 diffswitches = diffstart + diffstop
720 diffswitches.sort()
721 textdiff = ""
722 textnest = 0
723 textpos = 0
724 spanempty = False
725 for i, switch, tag in diffswitches:
726 textsection = self.escapetext(text[textpos:i])
727 textdiff += textsection
728 if textsection:
729 spanempty = False
730 if switch == 'start':
731 textnest += 1
732 elif switch == 'stop':
733 textnest -= 1
734 if switch == 'start' and textnest == 1:
735 # start of a textition
736 textdiff += "<span class='translate-diff-%s'>" % tag
737 spanempty = True
738 elif switch == 'stop' and textnest == 0:
739 # start of an equals block
740 if spanempty:
741 # FIXME: work out why kid swallows empty spans, and browsers display them horribly, then remove this
742 textdiff += "()"
743 textdiff += "</span>"
744 textpos = i
745 textdiff += self.escapetext(text[textpos:])
746 return textdiff
748 def getdiffcodes(self, cmp1, cmp2):
749 """compares the two strings and returns opcodes"""
750 return difflib.SequenceMatcher(None, cmp1, cmp2).get_opcodes()
752 def gettransreview(self, item, trans, suggestions):
753 """returns a widget for reviewing the given item's suggestions"""
754 hasplurals = len(trans) > 1
755 diffcodes = {}
756 for pluralitem, pluraltrans in enumerate(trans):
757 if isinstance(pluraltrans, str):
758 trans[pluralitem] = pluraltrans.decode("utf-8")
759 for suggestion in suggestions:
760 for pluralitem, pluralsugg in enumerate(suggestion):
761 if isinstance(pluralsugg, str):
762 suggestion[pluralitem] = pluralsugg.decode("utf-8")
763 forms = []
764 for pluralitem, pluraltrans in enumerate(trans):
765 pluraldiffcodes = [self.getdiffcodes(pluraltrans, suggestion[pluralitem]) for suggestion in suggestions]
766 diffcodes[pluralitem] = pluraldiffcodes
767 combineddiffs = reduce(list.__add__, pluraldiffcodes, [])
768 transdiff = self.highlightdiffs(pluraltrans, combineddiffs, issrc=True)
769 form = {"n": pluralitem, "diff": transdiff, "title": None}
770 if hasplurals:
771 pluralform = self.localize("Plural Form %d", pluralitem)
772 form["title"] = pluralform
773 forms.append(form)
774 transdict = {
775 "current_title": self.localize("Current Translation:"),
776 "editlink": self.geteditlink(item),
777 "forms": forms,
778 "isplural": hasplurals or None,
779 "itemid": "trans%d" % item,
781 suggitems = []
782 for suggid, msgstr in enumerate(suggestions):
783 suggestedby = self.project.getsuggester(self.pofilename, item, suggid)
784 if len(suggestions) > 1:
785 if suggestedby:
786 # l10n: First parameter: number
787 # l10n: Second parameter: name of translator
788 suggtitle = self.localize("Suggestion %d by %s:", suggid+1, suggestedby)
789 else:
790 suggtitle = self.localize("Suggestion %d:", suggid+1)
791 else:
792 if suggestedby:
793 # l10n: parameter: name of translator
794 suggtitle = self.localize("Suggestion by %s:", suggestedby)
795 else:
796 suggtitle = self.localize("Suggestion:")
797 forms = []
798 for pluralitem, pluraltrans in enumerate(trans):
799 pluralsuggestion = msgstr[pluralitem]
800 suggdiffcodes = diffcodes[pluralitem][suggid]
801 suggdiff = self.highlightdiffs(pluralsuggestion, suggdiffcodes, issrc=False)
802 if isinstance(pluralsuggestion, str):
803 pluralsuggestion = pluralsuggestion.decode("utf8")
804 form = {"diff": suggdiff}
805 form["suggid"] = "suggest%d.%d.%d" % (item, suggid, pluralitem)
806 form["value"] = pluralsuggestion
807 if hasplurals:
808 form["title"] = self.localize("Plural Form %d", pluralitem)
809 forms.append(form)
810 suggdict = {"title": suggtitle,
811 "forms": forms,
812 "suggid": "%d.%d" % (item, suggid),
813 "canreview": "review" in self.rights,
814 "back": None,
815 "skip": None,
817 suggitems.append(suggdict)
818 # l10n: verb
819 backbutton = {"item": item, "text": self.localize("Back")}
820 skipbutton = {"item": item, "text": self.localize("Skip")}
821 if suggitems:
822 suggitems[-1]["back"] = backbutton
823 suggitems[-1]["skip"] = skipbutton
824 else:
825 transdict["back"] = backbutton
826 transdict["skip"] = skipbutton
827 transdict["suggestions"] = suggitems
828 return transdict
830 def gettransview(self, item, trans, textarea=False):
831 """returns a widget for viewing the given item's translation"""
832 if textarea:
833 escapefunction = self.escapefortextarea
834 else:
835 escapefunction = self.escapetext
836 editlink = self.geteditlink(item)
837 transdict = {"editlink": editlink}
838 if len(trans) > 1:
839 forms = []
840 for pluralitem, pluraltext in enumerate(trans):
841 form = {"title": self.localize("Plural Form %d", pluralitem), "n": pluralitem, "text": escapefunction(pluraltext)}
842 forms.append(form)
843 transdict["forms"] = forms
844 elif trans:
845 transdict["text"] = escapefunction(trans[0])
846 else:
847 # Error, problem with plurals perhaps?
848 transdict["text"] = ""
849 return transdict
851 def getaltsrcdict(self, origdict):
852 # TODO: handle plurals !!
853 altsrcdict = {"available": False}
854 if self.altproject is not None:
855 altsrcdict["languagecode"] = pagelayout.weblanguage(self.altproject.languagecode)
856 altsrcdict["languagename"] = self.altproject.potree.getlanguagename(self.altproject.languagecode)
857 altsrcdict["dir"] = pagelayout.languagedir(altsrcdict["languagecode"])
858 altsrcdict["title"] = self.session.tr_lang(altsrcdict["languagename"])
859 if not origdict["isplural"]:
860 altsrctext = self.altproject.ugettext(origdict["text"])
861 if not origdict["isplural"] and altsrctext != origdict["text"] and not self.reviewmode:
862 altsrcdict["text"] = altsrctext
863 altsrcdict["available"] = True
864 return altsrcdict