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 else: 136 targetnode.text = data.forceunicode(text) or u""
137
138 - def gettarget(self):
139 targetnode = self._gettargetnode() 140 if targetnode is None: 141 etree.SubElement(self.xmlelement, self.namespaced("translation")) 142 return None 143 if self.hasplural(): 144 numerus_nodes = targetnode.findall(self.namespaced("numerusform")) 145 return multistring([node.text or u"" for node in numerus_nodes]) 146 else: 147 return data.forceunicode(targetnode.text) or u""
148 target = property(gettarget, settarget) 149 rich_target = property(base.TranslationUnit._get_rich_target, base.TranslationUnit._set_rich_target) 150
151 - def hasplural(self):
152 return self.xmlelement.get("numerus") == "yes"
153
154 - def addnote(self, text, origin=None, position="append"):
155 """Add a note specifically in a "comment" tag""" 156 if isinstance(text, str): 157 text = text.decode("utf-8") 158 current_notes = self.getnotes(origin) 159 self.removenotes(origin) 160 if origin in ["programmer", "developer", "source code"]: 161 note = etree.SubElement(self.xmlelement, self.namespaced("extracomment")) 162 else: 163 note = etree.SubElement(self.xmlelement, self.namespaced("translatorcomment")) 164 if position == "append": 165 note.text = "\n".join(filter(None, [current_notes, text.strip()])) 166 else: 167 note.text = text.strip()
168
169 - def getnotes(self, origin=None):
170 #TODO: consider only responding when origin has certain values 171 comments = [] 172 if origin in ["programmer", "developer", "source code", None]: 173 notenode = self.xmlelement.find(self.namespaced("comment")) 174 if notenode is not None: 175 comments.append(notenode.text) 176 notenode = self.xmlelement.find(self.namespaced("extracomment")) 177 if notenode is not None: 178 comments.append(notenode.text) 179 if origin in ["translator", None]: 180 notenode = self.xmlelement.find(self.namespaced("translatorcomment")) 181 if notenode is not None: 182 comments.append(notenode.text) 183 return '\n'.join(comments)
184
185 - def removenotes(self, origin=None):
186 """Remove all the translator notes.""" 187 if origin in ["programmer", "developer", "source code", None]: 188 note = self.xmlelement.find(self.namespaced("comment")) 189 if not note is None: 190 self.xmlelement.remove(note) 191 note = self.xmlelement.find(self.namespaced("extracomment")) 192 if not note is None: 193 self.xmlelement.remove(note) 194 if origin in ["translator", None]: 195 note = self.xmlelement.find(self.namespaced("translatorcomment")) 196 if not note is None: 197 self.xmlelement.remove(note)
198
199 - def _gettype(self):
200 """Returns the type of this translation.""" 201 targetnode = self._gettargetnode() 202 if targetnode is not None: 203 return targetnode.get("type") 204 return None
205
206 - def _settype(self, value=None):
207 """Set the type of this translation.""" 208 if value: 209 self._gettargetnode().set("type", value) 210 elif self._gettype(): 211 # lxml recommends against using .attrib, but there seems to be no 212 # other way 213 self._gettargetnode().attrib.pop("type")
214
215 - def isreview(self):
216 """States whether this unit needs to be reviewed""" 217 return self._gettype() == "unfinished"
218
219 - def isfuzzy(self):
220 return self._gettype() == "unfinished"
221
222 - def markfuzzy(self, value=True):
223 if value: 224 self._settype("unfinished") 225 else: 226 self._settype(None)
227
228 - def getid(self):
229 context_name = self.getcontext() 230 #XXX: context_name is not supposed to be able to be None (the <name> 231 # tag is compulsary in the <context> tag) 232 if context_name is not None: 233 return context_name + self.source 234 else: 235 return self.source
236
237 - def istranslatable(self):
238 # Found a file in the wild with no context and an empty source. This 239 # served as a header, so let's classify this as not translatable. 240 # http://bibletime.svn.sourceforge.net/viewvc/bibletime/trunk/bibletime/i18n/messages/bibletime_ui.ts 241 # Furthermore, let's decide to handle obsolete units as untranslatable 242 # like we do with PO. 243 return bool(self.getid()) and not self.isobsolete()
244
245 - def getcontext(self):
246 parent = self.xmlelement.getparent() 247 if parent is None: 248 return None 249 context = parent.find("name") 250 if context is None: 251 return None 252 return context.text
253
254 - def addlocation(self, location):
255 if isinstance(location, str): 256 location = location.decode("utf-8") 257 newlocation = etree.SubElement(self.xmlelement, self.namespaced("location")) 258 try: 259 filename, line = location.split(':', 1) 260 except ValueError: 261 filename = location 262 line = None 263 newlocation.set("filename", filename) 264 if line is not None: 265 newlocation.set("line", line)
266
267 - def getlocations(self):
268 location_tags = self.xmlelement.iterfind(self.namespaced("location")) 269 locations = [] 270 for location_tag in location_tags: 271 location = location_tag.get("filename") 272 line = location_tag.get("line") 273 if line is not None: 274 location += ':' + line 275 locations.append(location) 276 return locations
277
278 - def merge(self, otherunit, overwrite=False, comments=True, authoritative=False):
279 super(tsunit, self).merge(otherunit, overwrite, comments) 280 #TODO: check if this is necessary: 281 if otherunit.isfuzzy(): 282 self.markfuzzy()
283
284 - def isobsolete(self):
285 return self._gettype() == "obsolete"
286 287
288 -class tsfile(lisa.LISAfile):
289 """Class representing a XLIFF file store.""" 290 UnitClass = tsunit 291 Name = _("Qt Linguist Translation File") 292 Mimetypes = ["application/x-linguist"] 293 Extensions = ["ts"] 294 rootNode = "TS" 295 # We will switch out .body to fit with the context we are working on 296 bodyNode = "context" 297 XMLskeleton = '''<!DOCTYPE TS> 298 <TS> 299 </TS> 300 ''' 301 namespace = '' 302
303 - def __init__(self, *args, **kwargs):
304 self._contextname = None 305 lisa.LISAfile.__init__(self, *args, **kwargs)
306
307 - def initbody(self):
308 """Initialises self.body.""" 309 self.namespace = self.document.getroot().nsmap.get(None, None) 310 self.header = self.document.getroot() 311 if self._contextname: 312 self.body = self.getcontextnode(self._contextname) 313 else: 314 self.body = self.document.getroot()
315
316 - def getsourcelanguage(self):
317 """Get the source language for this .ts file. 318 319 The 'sourcelanguage' attribute was only added to the TS format in 320 Qt v4.5. We return 'en' if there is no sourcelanguage set. 321 322 We don't implement setsourcelanguage as users really shouldn't be 323 altering the source language in .ts files, it should be set correctly 324 by the extraction tools. 325 326 @return: ISO code e.g. af, fr, pt_BR 327 @rtype: String 328 """ 329 lang = data.normalize_code(self.header.get('sourcelanguage', "en")) 330 if lang == 'en-us': 331 return 'en' 332 return lang
333
334 - def gettargetlanguage(self):
335 """Get the target language for this .ts file. 336 337 @return: ISO code e.g. af, fr, pt_BR 338 @rtype: String 339 """ 340 return data.normalize_code(self.header.get('language'))
341
342 - def settargetlanguage(self, targetlanguage):
343 """Set the target language for this .ts file to L{targetlanguage}. 344 345 @param targetlanguage: ISO code e.g. af, fr, pt_BR 346 @type targetlanguage: String 347 """ 348 if targetlanguage: 349 self.header.set('language', targetlanguage)
350
351 - def _createcontext(self, contextname, comment=None):
352 """Creates a context node with an optional comment""" 353 context = etree.SubElement(self.document.getroot(), self.namespaced(self.bodyNode)) 354 name = etree.SubElement(context, self.namespaced("name")) 355 name.text = contextname 356 if comment: 357 comment_node = context.SubElement(context, "comment") 358 comment_node.text = comment 359 return context
360
361 - def _getcontextname(self, contextnode):
362 """Returns the name of the given context node.""" 363 return contextnode.find(self.namespaced("name")).text
364
365 - def _getcontextnames(self):
366 """Returns all contextnames in this TS file.""" 367 contextnodes = self.document.findall(self.namespaced("context")) 368 contextnames = [self.getcontextname(contextnode) for contextnode in contextnodes] 369 return contextnames
370
371 - def _getcontextnode(self, contextname):
372 """Returns the context node with the given name.""" 373 contextnodes = self.document.findall(self.namespaced("context")) 374 for contextnode in contextnodes: 375 if self._getcontextname(contextnode) == contextname: 376 return contextnode 377 return None
378
379 - def addunit(self, unit, new=True, contextname=None, createifmissing=True):
380 """Adds the given unit to the last used body node (current context). 381 382 If the contextname is specified, switch to that context (creating it 383 if allowed by createifmissing).""" 384 if contextname is None: 385 contextname = unit.getcontext() 386 387 if self._contextname != contextname: 388 if not self._switchcontext(contextname, createifmissing): 389 return None 390 super(tsfile, self).addunit(unit, new) 391 # lisa.setXMLspace(unit.xmlelement, "preserve") 392 return unit
393
394 - def _switchcontext(self, contextname, createifmissing=False):
395 """Switch the current context to the one named contextname, optionally 396 creating it if it doesn't exist.""" 397 self._contextname = contextname 398 contextnode = self._getcontextnode(contextname) 399 if contextnode is None: 400 if not createifmissing: 401 return False 402 contextnode = self._createcontext(contextname) 403 404 self.body = contextnode 405 if self.body is None: 406 return False 407 return True
408
409 - def nplural(self):
410 lang = self.header.get("language") 411 if NPLURALS.has_key(lang): 412 return NPLURALS[lang] 413 else: 414 return 1
415
416 - def __str__(self):
417 """Converts to a string containing the file's XML. 418 419 We have to override this to ensure mimic the Qt convention: 420 - no XML decleration 421 - plain DOCTYPE that lxml seems to ignore 422 """ 423 # A bug in lxml means we have to output the doctype ourselves. For 424 # more information, see: 425 # http://codespeak.net/pipermail/lxml-dev/2008-October/004112.html 426 # The problem was fixed in lxml 2.1.3 427 output = etree.tostring(self.document, pretty_print=True, 428 xml_declaration=False, encoding='utf-8') 429 if not "<!DOCTYPE TS>" in output[:30]: 430 output = "<!DOCTYPE TS>" + output 431 return output
432