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