translate.storage.ts2

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