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 -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