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