Package translate :: Package storage :: Module ts2
[hide private]
[frames] | no frames]

Source Code for Module translate.storage.ts2

  1  #!/usr/bin/env python 
  2  # -*- coding: utf-8 -*- 
  3  # 
  4  # Copyright 2008-2009 Zuza Software Foundation 
  5  # 
  6  # This file is part of the Translate Toolkit. 
  7  # 
  8  # This program 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. 
 12  # 
 13  # This program 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. 
 17  # 
 18  # You should have received a copy of the GNU General Public License 
 19  # along with this program; if not, see <http://www.gnu.org/licenses/>. 
 20   
 21  """Module for handling Qt linguist (.ts) files. 
 22   
 23  This will eventually replace the older ts.py which only supports the older 
 24  format. While converters haven't been updated to use this module, we retain 
 25  both. 
 26   
 27  U{TS file format 4.3<http://doc.trolltech.com/4.3/linguist-ts-file-format.html>}, 
 28  U{4.5<http://doc.trolltech.com/4.5/linguist-ts-file-format.html>}, 
 29  U{Example<http://svn.ez.no/svn/ezcomponents/trunk/Translation/docs/linguist-format.txt>}, 
 30  U{Plurals forms<http://www.koders.com/cpp/fidE7B7E83C54B9036EB7FA0F27BC56BCCFC4B9DF34.aspx#L200>} 
 31   
 32  U{Specification of the valid variable entries <http://doc.trolltech.com/4.3/qstring.html#arg>}, 
 33  U{2 <http://doc.trolltech.com/4.3/qstring.html#arg-2>} 
 34  """ 
 35   
 36  from lxml import etree 
 37   
 38  from translate.lang import data 
 39  from translate.misc.multistring import multistring 
 40  from translate.storage import base, lisa 
 41  from translate.storage.placeables import general 
 42  from translate.storage.workflow import StateEnum as state 
 43   
 44  # TODO: handle translation types 
 45   
 46  NPLURALS = { 
 47  'jp': 1, 
 48  'en': 2, 
 49  'fr': 2, 
 50  'lv': 3, 
 51  'ga': 3, 
 52  'cs': 3, 
 53  'sk': 3, 
 54  'mk': 3, 
 55  'lt': 3, 
 56  'ru': 3, 
 57  'pl': 3, 
 58  'ro': 3, 
 59  'sl': 4, 
 60  'mt': 4, 
 61  'cy': 5, 
 62  'ar': 6, 
 63  } 
 64   
 65   
66 -class tsunit(lisa.LISAunit):
67 """A single term in the xliff file.""" 68 69 rootNode = "message" 70 languageNode = "source" 71 textNode = "" 72 namespace = '' 73 rich_parsers = general.parsers 74 75 S_OBSOLETE = state.OBSOLETE 76 S_UNTRANSLATED = state.EMPTY 77 S_FUZZY = state.NEEDS_WORK 78 S_TRANSLATED = state.UNREVIEWED 79 80 statemap = { 81 "obsolete": S_OBSOLETE, 82 "unfinished": S_FUZZY, 83 "": S_TRANSLATED, 84 None: S_TRANSLATED, 85 } 86 """This maps the unit "type" attribute to state.""" 87 88 STATE = { 89 S_OBSOLETE: (state.OBSOLETE, state.EMPTY), 90 S_UNTRANSLATED: (state.EMPTY, state.NEEDS_WORK), 91 S_FUZZY: (state.NEEDS_WORK, state.UNREVIEWED), 92 S_TRANSLATED: (state.UNREVIEWED, state.MAX), 93 } 94 95 statemap_r = dict((i[1], i[0]) for i in statemap.iteritems()) 96
97 - def createlanguageNode(self, lang, text, purpose):
98 """Returns an xml Element setup with given parameters.""" 99 100 assert purpose 101 if purpose == "target": 102 purpose = "translation" 103 langset = etree.Element(self.namespaced(purpose)) 104 #TODO: check language 105 # lisa.setXMLlang(langset, lang) 106 107 langset.text = text 108 return langset
109
110 - def _getsourcenode(self):
111 return self.xmlelement.find(self.namespaced(self.languageNode))
112
113 - def _gettargetnode(self):
114 return self.xmlelement.find(self.namespaced("translation"))
115
116 - def getlanguageNodes(self):
117 """We override this to get source and target nodes.""" 118 119 def not_none(node): 120 return not node is None
121 return filter(not_none, [self._getsourcenode(), self._gettargetnode()])
122
123 - def getsource(self):
124 # TODO: support <byte>. See bug 528. 125 sourcenode = self._getsourcenode() 126 if self.hasplural(): 127 return multistring([sourcenode.text]) 128 else: 129 return data.forceunicode(sourcenode.text)
130 source = property(getsource, lisa.LISAunit.setsource) 131 rich_source = property(base.TranslationUnit._get_rich_source, base.TranslationUnit._set_rich_source) 132
133 - def settarget(self, text):
134 # This is a fairly destructive implementation. Don't assume that this 135 # is necessarily correct in all regards, but it does deal with a lot of 136 # cases. It is hard to deal with plurals. 137 # 138 # Firstly deal with reinitialising to None or setting to identical 139 # string. 140 self._rich_target = None 141 if self.gettarget() == text: 142 return 143 strings = [] 144 if isinstance(text, multistring): 145 strings = text.strings 146 elif isinstance(text, list): 147 strings = text 148 else: 149 strings = [text] 150 targetnode = self._gettargetnode() 151 type = targetnode.get("type") 152 targetnode.clear() 153 if type: 154 targetnode.set("type", type) 155 if self.hasplural() or len(strings) > 1: 156 self.xmlelement.set("numerus", "yes") 157 for string in strings: 158 numerus = etree.SubElement(targetnode, self.namespaced("numerusform")) 159 numerus.text = data.forceunicode(string) or u"" 160 # manual, nasty pretty printing. See bug 1420. 161 numerus.tail = u"\n " 162 else: 163 targetnode.text = data.forceunicode(text) or u"" 164 targetnode.tail = u"\n "
165
166 - def gettarget(self):
167 targetnode = self._gettargetnode() 168 if targetnode is None: 169 etree.SubElement(self.xmlelement, self.namespaced("translation")) 170 return None 171 if self.hasplural(): 172 numerus_nodes = targetnode.findall(self.namespaced("numerusform")) 173 return multistring([node.text or u"" for node in numerus_nodes]) 174 else: 175 return data.forceunicode(targetnode.text) or u""
176 target = property(gettarget, settarget) 177 rich_target = property(base.TranslationUnit._get_rich_target, base.TranslationUnit._set_rich_target) 178
179 - def hasplural(self):
180 return self.xmlelement.get("numerus") == "yes"
181
182 - def addnote(self, text, origin=None, position="append"):
183 """Add a note specifically in a "comment" tag""" 184 if isinstance(text, str): 185 text = text.decode("utf-8") 186 current_notes = self.getnotes(origin) 187 self.removenotes(origin) 188 if origin in ["programmer", "developer", "source code"]: 189 note = etree.SubElement(self.xmlelement, self.namespaced("extracomment")) 190 else: 191 note = etree.SubElement(self.xmlelement, self.namespaced("translatorcomment")) 192 if position == "append": 193 note.text = "\n".join(filter(None, [current_notes, text.strip()])) 194 else: 195 note.text = text.strip()
196
197 - def getnotes(self, origin=None):
198 #TODO: consider only responding when origin has certain values 199 comments = [] 200 if origin in ["programmer", "developer", "source code", None]: 201 notenode = self.xmlelement.find(self.namespaced("comment")) 202 if notenode is not None and notenode.text is not None: 203 comments.append(notenode.text) 204 notenode = self.xmlelement.find(self.namespaced("extracomment")) 205 if notenode is not None and notenode.text is not None: 206 comments.append(notenode.text) 207 if origin in ["translator", None]: 208 notenode = self.xmlelement.find(self.namespaced("translatorcomment")) 209 if notenode is not None and notenode.text is not None: 210 comments.append(notenode.text) 211 return '\n'.join(comments)
212
213 - def removenotes(self, origin=None):
214 """Remove all the translator notes.""" 215 if origin in ["programmer", "developer", "source code", None]: 216 note = self.xmlelement.find(self.namespaced("comment")) 217 if not note is None: 218 self.xmlelement.remove(note) 219 note = self.xmlelement.find(self.namespaced("extracomment")) 220 if not note is None: 221 self.xmlelement.remove(note) 222 if origin in ["translator", None]: 223 note = self.xmlelement.find(self.namespaced("translatorcomment")) 224 if not note is None: 225 self.xmlelement.remove(note)
226
227 - def _gettype(self):
228 """Returns the type of this translation.""" 229 targetnode = self._gettargetnode() 230 if targetnode is not None: 231 return targetnode.get("type") 232 return None
233
234 - def _settype(self, value=None):
235 """Set the type of this translation.""" 236 if value: 237 self._gettargetnode().set("type", value) 238 elif self._gettype(): 239 # lxml recommends against using .attrib, but there seems to be no 240 # other way 241 self._gettargetnode().attrib.pop("type")
242
243 - def isreview(self):
244 """States whether this unit needs to be reviewed""" 245 return self._gettype() == "unfinished"
246
247 - def isfuzzy(self):
248 return self._gettype() == "unfinished" and self.target
249
250 - def markfuzzy(self, value=True):
251 if value: 252 self._settype("unfinished") 253 else: 254 self._settype(None)
255
256 - def getid(self):
257 if self.source is None: 258 return None 259 context_name = self.getcontext() 260 #XXX: context_name is not supposed to be able to be None (the <name> 261 # tag is compulsary in the <context> tag) 262 if context_name is not None: 263 return context_name + self.source 264 else: 265 return self.source
266
267 - def istranslatable(self):
268 # Found a file in the wild with no context and an empty source. This 269 # served as a header, so let's classify this as not translatable. 270 # http://bibletime.svn.sourceforge.net/viewvc/bibletime/trunk/bibletime/i18n/messages/bibletime_ui.ts 271 # Furthermore, let's decide to handle obsolete units as untranslatable 272 # like we do with PO. 273 return bool(self.getid()) and not self.isobsolete()
274
275 - def getcontext(self):
276 parent = self.xmlelement.getparent() 277 if parent is None: 278 return None 279 context = parent.find("name") 280 if context is None: 281 return None 282 return context.text
283
284 - def addlocation(self, location):
285 if isinstance(location, str): 286 location = location.decode("utf-8") 287 newlocation = etree.SubElement(self.xmlelement, self.namespaced("location")) 288 try: 289 filename, line = location.split(':', 1) 290 except ValueError: 291 filename = location 292 line = None 293 newlocation.set("filename", filename) 294 if line is not None: 295 newlocation.set("line", line)
296
297 - def getlocations(self):
298 location_tags = self.xmlelement.iterfind(self.namespaced("location")) 299 locations = [] 300 for location_tag in location_tags: 301 location = location_tag.get("filename") 302 line = location_tag.get("line") 303 if line: 304 if location: 305 location += ':' + line 306 else: 307 location = line 308 locations.append(location) 309 return locations
310
311 - def merge(self, otherunit, overwrite=False, comments=True, authoritative=False):
312 super(tsunit, self).merge(otherunit, overwrite, comments) 313 #TODO: check if this is necessary: 314 if otherunit.isfuzzy(): 315 self.markfuzzy()
316
317 - def isobsolete(self):
318 return self._gettype() == "obsolete"
319
320 - def get_state_n(self):
321 type = self._gettype() 322 if type == "unfinished": 323 # We want to distinguish between fuzzy and untranslated, which the 324 # format doesn't really do 325 if self.target: 326 return self.S_FUZZY 327 else: 328 return self.S_UNTRANSLATED 329 return self.statemap[type]
330
331 - def set_state_n(self, value):
332 if value not in self.statemap_r: 333 value = self.get_state_id(value) 334 335 if value == self.S_UNTRANSLATED: 336 # No real way of representing that in the format, so we just 337 # handle it the same as unfinished 338 value = self.S_FUZZY 339 self._settype(self.statemap_r[value])
340 341
342 -class tsfile(lisa.LISAfile):
343 """Class representing a XLIFF file store.""" 344 UnitClass = tsunit 345 Name = _("Qt Linguist Translation File") 346 Mimetypes = ["application/x-linguist"] 347 Extensions = ["ts"] 348 rootNode = "TS" 349 # We will switch out .body to fit with the context we are working on 350 bodyNode = "context" 351 XMLskeleton = '''<!DOCTYPE TS> 352 <TS> 353 </TS> 354 ''' 355 namespace = '' 356
357 - def __init__(self, *args, **kwargs):
358 self._contextname = None 359 lisa.LISAfile.__init__(self, *args, **kwargs)
360
361 - def initbody(self):
362 """Initialises self.body.""" 363 self.namespace = self.document.getroot().nsmap.get(None, None) 364 self.header = self.document.getroot() 365 if self._contextname: 366 self.body = self.getcontextnode(self._contextname) 367 else: 368 self.body = self.document.getroot()
369
370 - def getsourcelanguage(self):
371 """Get the source language for this .ts file. 372 373 The 'sourcelanguage' attribute was only added to the TS format in 374 Qt v4.5. We return 'en' if there is no sourcelanguage set. 375 376 We don't implement setsourcelanguage as users really shouldn't be 377 altering the source language in .ts files, it should be set correctly 378 by the extraction tools. 379 380 @return: ISO code e.g. af, fr, pt_BR 381 @rtype: String 382 """ 383 lang = data.normalize_code(self.header.get('sourcelanguage', "en")) 384 if lang == 'en-us': 385 return 'en' 386 return lang
387
388 - def gettargetlanguage(self):
389 """Get the target language for this .ts file. 390 391 @return: ISO code e.g. af, fr, pt_BR 392 @rtype: String 393 """ 394 return data.normalize_code(self.header.get('language'))
395
396 - def settargetlanguage(self, targetlanguage):
397 """Set the target language for this .ts file to L{targetlanguage}. 398 399 @param targetlanguage: ISO code e.g. af, fr, pt_BR 400 @type targetlanguage: String 401 """ 402 if targetlanguage: 403 self.header.set('language', targetlanguage)
404
405 - def _createcontext(self, contextname, comment=None):
406 """Creates a context node with an optional comment""" 407 context = etree.SubElement(self.document.getroot(), self.namespaced(self.bodyNode)) 408 name = etree.SubElement(context, self.namespaced("name")) 409 name.text = contextname 410 if comment: 411 comment_node = context.SubElement(context, "comment") 412 comment_node.text = comment 413 return context
414
415 - def _getcontextname(self, contextnode):
416 """Returns the name of the given context node.""" 417 return contextnode.find(self.namespaced("name")).text
418
419 - def _getcontextnames(self):
420 """Returns all contextnames in this TS file.""" 421 contextnodes = self.document.findall(self.namespaced("context")) 422 contextnames = [self.getcontextname(contextnode) for contextnode in contextnodes] 423 return contextnames
424
425 - def _getcontextnode(self, contextname):
426 """Returns the context node with the given name.""" 427 contextnodes = self.document.findall(self.namespaced("context")) 428 for contextnode in contextnodes: 429 if self._getcontextname(contextnode) == contextname: 430 return contextnode 431 return None
432
433 - def addunit(self, unit, new=True, contextname=None, createifmissing=True):
434 """Adds the given unit to the last used body node (current context). 435 436 If the contextname is specified, switch to that context (creating it 437 if allowed by createifmissing).""" 438 if contextname is None: 439 contextname = unit.getcontext() 440 441 if self._contextname != contextname: 442 if not self._switchcontext(contextname, createifmissing): 443 return None 444 super(tsfile, self).addunit(unit, new) 445 # lisa.setXMLspace(unit.xmlelement, "preserve") 446 return unit
447
448 - def _switchcontext(self, contextname, createifmissing=False):
449 """Switch the current context to the one named contextname, optionally 450 creating it if it doesn't exist.""" 451 self._contextname = contextname 452 contextnode = self._getcontextnode(contextname) 453 if contextnode is None: 454 if not createifmissing: 455 return False 456 contextnode = self._createcontext(contextname) 457 458 self.body = contextnode 459 if self.body is None: 460 return False 461 return True
462
463 - def nplural(self):
464 lang = self.header.get("language") 465 if lang in NPLURALS: 466 return NPLURALS[lang] 467 else: 468 return 1
469
470 - def __str__(self):
471 """Converts to a string containing the file's XML. 472 473 We have to override this to ensure mimic the Qt convention: 474 - no XML decleration 475 - plain DOCTYPE that lxml seems to ignore 476 """ 477 # A bug in lxml means we have to output the doctype ourselves. For 478 # more information, see: 479 # http://codespeak.net/pipermail/lxml-dev/2008-October/004112.html 480 # The problem was fixed in lxml 2.1.3 481 output = etree.tostring(self.document, pretty_print=True, 482 xml_declaration=False, encoding='utf-8') 483 if not "<!DOCTYPE TS>" in output[:30]: 484 output = "<!DOCTYPE TS>" + output 485 return output
486