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