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