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