Package translate :: Package storage :: Module xliff

[hide private]
[frames] | no frames]

# Source Code for Module translate.storage.xliff

      1  #!/usr/bin/env python
      2  # -*- coding: utf-8 -*-
      3  #
      4  # Copyright 2005-2007 Zuza Software Foundation
      5  #
      6  # This file is part of translate.
      7  #
      8  # translate 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  # translate 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 translate; if not, write to the Free Software
     20  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
     21  #
     22
     23  """Module for handling XLIFF files for translation.
     24
     25  The official recommendation is to use the extention .xlf for XLIFF files.
     26  """
     27
     28  from translate.storage import base
     29  from translate.storage import lisa
     30  from lxml import etree
     31
     32  # TODO: handle translation types
     33
     34 -class xliffunit(lisa.LISAunit):
     35      """A single term in the xliff file."""
     36
     37      rootNode = "trans-unit"
     38      languageNode = "source"
     39      textNode = ""
     40      namespace = 'urn:oasis:names:tc:xliff:document:1.1'
     41
     42      #TODO: id and all the trans-unit level stuff
     43
     44 -    def createlanguageNode(self, lang, text, purpose):
     45          """Returns an xml Element setup with given parameters."""
     46
     47          #TODO: for now we do source, but we have to test if it is target, perhaps
     48          # with parameter. Alternatively, we can use lang, if supplied, since an xliff
     49          #file has to conform to the bilingual nature promised by the header.
     50          assert purpose
     51          langset = etree.Element(self.namespaced(purpose))
     52          #TODO: check language
     53  #        lisa.setXMLlang(langset, lang)
     54
     55  #        self.createPHnodes(langset, text)
     56          langset.text = text
     57          return langset
     58
     59 -    def getlanguageNodes(self):
     60          """We override this to get source and target nodes."""
     61          sources = self.xmlelement.findall('.//%s' % self.namespaced(self.languageNode))
     62          targets = self.xmlelement.findall('.//%s' % self.namespaced('target'))
     63          sourcesl = len(sources)
     64          targetsl = len(targets)
     65          nodes = []
     66          for pair in zip(sources, targets):
     67              nodes.extend(list(pair))
     68          if sourcesl > targetsl:
     69              nodes.extend(sources[- (sourcesl - targetsl):])
     70          return nodes
     71
     72 -    def addalttrans(self, txt, origin=None, lang=None):
     73          """Adds an alt-trans tag and alt-trans components to the unit.
     74
     75          @type txt: String
     76          @param txt: Alternative translation of the source text.
     77          """
     78
     79          #TODO: support adding a source tag ad match quality attribute.  At
     80          # the source tag is needed to inject fuzzy matches from a TM.
     81          if isinstance(txt, str):
     82              txt = txt.decode("utf-8")
     83          alttrans = etree.SubElement(self.xmlelement, self.namespaced("alt-trans"))
     84          alttarget = etree.SubElement(alttrans, self.namespaced("target"))
     85          alttarget.text = txt
     86          if origin:
     87              alttrans.set("origin", origin)
     88          if lang:
     89              lisa.setXMLlang(alttrans, lang)
     90
     91 -    def getalttrans(self, origin=None):
     92          """Returns <alt-trans> for the given origin as a list of units. No
     93          origin means all alternatives."""
     94          translist = []
     95          for node in self.xmlelement.findall(".//%s" % self.namespaced("alt-trans")):
     96              if self.correctorigin(node, origin):
     97                  # We build some mini units that keep the xmlelement. This
     98                  # makes it easier to delete it if it is passed back to us.
     99                  newunit = base.TranslationUnit(self.source)
    100
    101                  # the source tag is optional
    102                  sourcenode = node.find(".//%s" % self.namespaced("source"))
    103                  if not sourcenode is None:
    104                      newunit.source = lisa.getText(sourcenode)
    105
    106                  # must have one or more targets
    107                  targetnode = node.find(".//%s" % self.namespaced("target"))
    108                  newunit.target = lisa.getText(targetnode)
    109                  #TODO: support multiple targets better
    110                  #TODO: support notes in alt-trans
    111                  newunit.xmlelement = node
    112
    113                  translist.append(newunit)
    114          return translist
    115
    116 -    def delalttrans(self, alternative):
    117          """Removes the supplied alternative from the list of alt-trans tags"""
    118          self.xmlelement.remove(alternative.xmlelement)
    119
    120 -    def addnote(self, text, origin=None):
    121          """Add a note specifically in a "note" tag"""
    122          if isinstance(text, str):
    123              text = text.decode("utf-8")
    124          note = etree.SubElement(self.xmlelement, self.namespaced("note"))
    125          note.text = text.strip()
    126          if origin:
    127              note.set("from", origin)
    128
    129 -    def getnotelist(self, origin=None):
    130          """Private method that returns the text from notes matching 'origin' or all notes."""
    131          notenodes = self.xmlelement.findall(".//%s" % self.namespaced("note"))
    132          # TODO: consider using xpath to construct initial_list directly
    133          # or to simply get the correct text from the outset (just remember to
    134          # check for duplication.
    135          initial_list = [lisa.getText(note) for note in notenodes if self.correctorigin(note, origin)]
    136
    137          # Remove duplicate entries from list:
    138          dictset = {}
    139          notelist = [dictset.setdefault(note, note) for note in initial_list if note not in dictset]
    140
    141          return notelist
    142
    143 -    def getnotes(self, origin=None):
    144          return '\n'.join(self.getnotelist(origin=origin))
    145
    146 -    def removenotes(self):
    147          """Remove all the translator notes."""
    148          notes = self.xmlelement.findall(".//%s" % self.namespaced("note"))
    149          for note in notes:
    150              if self.correctorigin(note, origin="translator"):
    151                  self.xmlelement.remove(note)
    152
    153 -    def adderror(self, errorname, errortext):
    154          """Adds an error message to this unit."""
    155          #TODO: consider factoring out: some duplication between XLIFF and TMX
    156          text = errorname + ': ' + errortext
    157          self.addnote(text, origin="pofilter")
    158
    159 -    def geterrors(self):
    160          """Get all error messages."""
    161          #TODO: consider factoring out: some duplication between XLIFF and TMX
    162          notelist = self.getnotelist(origin="pofilter")
    163          errordict = {}
    164          for note in notelist:
    165              errorname, errortext = note.split(': ')
    166              errordict[errorname] = errortext
    167          return errordict
    168
    169 -    def isapproved(self):
    170          """States whether this unit is approved."""
    171          return self.xmlelement.get("approved") == "yes"
    172
    173 -    def markapproved(self, value=True):
    174          """Mark this unit as approved."""
    175          if value:
    176              self.xmlelement.set("approved", "yes")
    177          elif self.isapproved():
    178              self.xmlelement.set("approved", "no")
    179
    180 -    def isreview(self):
    181          """States whether this unit needs to be reviewed"""
    182          targetnode = self.getlanguageNode(lang=None, index=1)
    183          return not targetnode is None and \
    184                  "needs-review" in targetnode.get("state", "")
    185
    186 -    def markreviewneeded(self, needsreview=True, explanation=None):
    187          """Marks the unit to indicate whether it needs review. Adds an optional explanation as a note."""
    188          targetnode = self.getlanguageNode(lang=None, index=1)
    189          if not targetnode is None:
    190              if needsreview:
    191                  targetnode.set("state", "needs-review-translation")
    192                  if explanation:
    193                      self.addnote(explanation, origin="translator")
    194              else:
    195                  del targetnode.attrib["state"]
    196
    197 -    def isfuzzy(self):
    198  #        targetnode = self.getlanguageNode(lang=None, index=1)
    199  #        return not targetnode is None and \
    200  #                (targetnode.get("state-qualifier") == "fuzzy-match" or \
    201  #                targetnode.get("state") == "needs-review-translation")
    202          return not self.isapproved()
    203
    204 -    def markfuzzy(self, value=True):
    205          if value:
    206              self.markapproved(False)
    207          else:
    208              self.markapproved(True)
    209          targetnode = self.getlanguageNode(lang=None, index=1)
    210          if not targetnode is None:
    211              if value:
    212                  targetnode.set("state", "needs-review-translation")
    213              else:
    214                  for attribute in ["state", "state-qualifier"]:
    215                      if attribute in targetnode.attrib:
    216                          del targetnode.attrib[attribute]
    217
    218 -    def settarget(self, text, lang='xx', append=False):
    219          """Sets the target string to the given value."""
    220          super(xliffunit, self).settarget(text, lang, append)
    221          if text:
    222              self.marktranslated()
    223
    224  # This code is commented while this will almost always return false.
    225  # This way pocount, etc. works well.
    226  #    def istranslated(self):
    227  #        targetnode = self.getlanguageNode(lang=None, index=1)
    228  #        return not targetnode is None and \
    229  #                (targetnode.get("state") == "translated")
    230
    231 -    def istranslatable(self):
    232          value = self.xmlelement.get("translate")
    233          if value and value.lower() == 'no':
    234              return False
    235          return True
    236
    237 -    def marktranslated(self):
    238          targetnode = self.getlanguageNode(lang=None, index=1)
    239          if targetnode is None:
    240              return
    241          if self.isfuzzy() and "state-qualifier" in targetnode.attrib:
    242              #TODO: consider
    243              del targetnode.attrib["state-qualifier"]
    244          targetnode.set("state", "translated")
    245
    246 -    def setid(self, id):
    247          self.xmlelement.set("id", id)
    248
    249 -    def getid(self):
    250          return self.xmlelement.get("id") or ""
    251
    252 -    def addlocation(self, location):
    253          self.setid(location)
    254
    255 -    def getlocations(self):
    256          return [self.getid()]
    257
    258 -    def createcontextgroup(self, name, contexts=None, purpose=None):
    259          """Add the context group to the trans-unit with contexts a list with
    260          (type, text) tuples describing each context."""
    261          assert contexts
    262          group = etree.Element(self.namespaced("context-group"))
    263          # context-group tags must appear at the start within <group>
    264          # tags. Otherwise it must be appended to the end of a group
    265          # of tags.
    266          if self.xmlelement.tag == self.namespaced("group"):
    267              self.xmlelement.insert(0, group)
    268          else:
    269              self.xmlelement.append(group)
    270          group.set("name", name)
    271          if purpose:
    272              group.set("purpose", purpose)
    273          for type, text in contexts:
    274              if isinstance(text, str):
    275                  text = text.decode("utf-8")
    276              context = etree.SubElement(group, self.namespaced("context"))
    277              context.text = text
    278              context.set("context-type", type)
    279
    280 -    def getcontextgroups(self, name):
    281          """Returns the contexts in the context groups with the specified name"""
    282          groups = []
    283          grouptags = self.xmlelement.findall(".//%s" % self.namespaced("context-group"))
    284          for group in grouptags:
    285              if group.get("name") == name:
    286                  contexts = group.findall(".//%s" % self.namespaced("context"))
    287                  pairs = []
    288                  for context in contexts:
    289                      pairs.append((context.get("context-type"), lisa.getText(context)))
    290                  groups.append(pairs) #not extend
    291          return groups
    292
    293 -    def getrestype(self):
    294          """returns the restype attribute in the trans-unit tag"""
    295          return self.xmlelement.get("restype")
    296
    297 -    def merge(self, otherunit, overwrite=False, comments=True):
    298          #TODO: consider other attributes like "approved"
    299          super(xliffunit, self).merge(otherunit, overwrite, comments)
    300          if self.target:
    301              self.marktranslated()
    302          if otherunit.isfuzzy():
    303              self.markfuzzy()
    304
    305 -    def correctorigin(self, node, origin):
    306          """Check against node tag's origin (e.g note or alt-trans)"""
    307          if origin == None:
    308              return True
    309          elif origin in node.get("from", ""):
    310              return True
    311          elif origin in node.get("origin", ""):
    312              return True
    313          else:
    314              return False
    315
    316 -class xlifffile(lisa.LISAfile):
    317      """Class representing a XLIFF file store."""
    318      UnitClass = xliffunit
    319      Name = "XLIFF file"
    320      Mimetypes  = ["application/x-xliff", "application/x-xliff+xml"]
    321      Extensions = ["xlf", "xliff"]
    322      rootNode = "xliff"
    323      bodyNode = "body"
    324      XMLskeleton = '''<?xml version="1.0" ?>
    325  <xliff version='1.1' xmlns='urn:oasis:names:tc:xliff:document:1.1'>
    326  <file original='NoName' source-language='en' datatype='plaintext'>
    327  <body>
    328  </body>
    329  </file>
    330  </xliff>'''
    331      namespace = 'urn:oasis:names:tc:xliff:document:1.1'
    332
    333 -    def __init__(self, *args, **kwargs):
    334          lisa.LISAfile.__init__(self, *args, **kwargs)
    335          self._filename = "NoName"
    336          self._messagenum = 0
    337
    338          # Allow the inputfile to override defaults for source and target language.
    339          filenode = self.document.find('.//%s' % self.namespaced('file'))
    340          sourcelanguage = filenode.get('source-language')
    341          if sourcelanguage:
    342              self.setsourcelanguage(sourcelanguage)
    343          targetlanguage = filenode.get('target-language')
    344          if targetlanguage:
    345              self.settargetlanguage(targetlanguage)
    346
    347 -    def addheader(self):
    348          """Initialise the file header."""
    349          filenode = self.document.find(self.namespaced("file"))
    350          filenode.set("source-language", self.sourcelanguage)
    351          if self.targetlanguage:
    352              filenode.set("target-language", self.targetlanguage)
    353
    354 -    def createfilenode(self, filename, sourcelanguage=None, targetlanguage=None, datatype='plaintext'):
    355          """creates a filenode with the given filename. All parameters are needed
    356          for XLIFF compliance."""
    357          self.removedefaultfile()
    358          if sourcelanguage is None:
    359              sourcelanguage = self.sourcelanguage
    360          if targetlanguage is None:
    361              targetlanguage = self.targetlanguage
    362          filenode = etree.Element(self.namespaced("file"))
    363          filenode.set("original", filename)
    364          filenode.set("source-language", sourcelanguage)
    365          if targetlanguage:
    366              filenode.set("target-language", targetlanguage)
    367          filenode.set("datatype", datatype)
    368          bodyNode = etree.SubElement(filenode, self.namespaced(self.bodyNode))
    369          return filenode
    370
    371 -    def getfilename(self, filenode):
    372          """returns the name of the given file"""
    373          return filenode.get("original")
    374
    375 -    def getfilenames(self):
    376          """returns all filenames in this XLIFF file"""
    377          filenodes = self.document.findall(self.namespaced("file"))
    378          filenames = [self.getfilename(filenode) for filenode in filenodes]
    379          filenames = filter(None, filenames)
    380          if len(filenames) == 1 and filenames[0] == '':
    381              filenames = []
    382          return filenames
    383
    384 -    def getfilenode(self, filename):
    385          """finds the filenode with the given name"""
    386          filenodes = self.document.findall(self.namespaced("file"))
    387          for filenode in filenodes:
    388              if self.getfilename(filenode) == filename:
    389                  return filenode
    390          return None
    391
    392 -    def getdatatype(self, filename=None):
    393          """Returns the datatype of the stored file. If no filename is given,
    394          the datatype of the first file is given."""
    395          if filename:
    396              node = self.getfilenode(filename)
    397              if not node is None:
    398                  return node.get("datatype")
    399          else:
    400              filenames = self.getfilenames()
    401              if len(filenames) > 0 and filenames[0] != "NoName":
    402                  return self.getdatatype(filenames[0])
    403          return ""
    404
    405 -    def getdate(self, filename=None):
    406          """Returns the date attribute for the file. If no filename is given,
    407          the date of the first file is given. If the date attribute is not
    408          specified, None is returned."""
    409          if filename:
    410              node = self.getfilenode(filename)
    411              if not node is None:
    412                  return node.get("date")
    413          else:
    414              filenames = self.getfilenames()
    415              if len(filenames) > 0 and filenames[0] != "NoName":
    416                  return self.getdate(filenames[0])
    417          return None
    418
    419 -    def removedefaultfile(self):
    420          """We want to remove the default file-tag as soon as possible if we
    421          know if still present and empty."""
    422          filenodes = self.document.findall(self.namespaced("file"))
    423          if len(filenodes) > 1:
    424              for filenode in filenodes:
    425                  if filenode.get("original") == "NoName" and \
    426                          not filenode.findall(".//%s" % self.namespaced(self.UnitClass.rootNode)):
    427                      self.document.getroot().remove(filenode)
    428                  break
    429
    430 -    def getheadernode(self, filenode, createifmissing=False):
    431          """finds the header node for the given filenode"""
    432          # TODO: Deprecated?
    433          headernode = list(filenode.find(self.namespaced("header")))
    434          if not headernode is None:
    435              return headernode
    436          if not createifmissing:
    437              return None
    438          headernode = etree.SubElement(filenode, self.namespaced("header"))
    439          return headernode
    440
    441 -    def getbodynode(self, filenode, createifmissing=False):
    442          """finds the body node for the given filenode"""
    443          bodynode = filenode.find(self.namespaced("body"))
    444          if not bodynode is None:
    445              return bodynode
    446          if not createifmissing:
    447              return None
    448          bodynode = etree.SubElement(filenode, self.namespaced("body"))
    449          return bodynode
    450
    451 -    def addsourceunit(self, source, filename="NoName", createifmissing=False):
    452          """adds the given trans-unit to the last used body node if the filename has changed it uses the slow method instead (will create the nodes required if asked). Returns success"""
    453          if self._filename != filename:
    454              if not self.switchfile(filename, createifmissing):
    455                  return None
    456          unit = super(xlifffile, self).addsourceunit(source)
    457          self._messagenum += 1
    458          unit.setid("%d" % self._messagenum)
    459          lisa.setXMLspace(unit.xmlelement, "preserve")
    460          return unit
    461
    462 -    def switchfile(self, filename, createifmissing=False):
    463          """adds the given trans-unit (will create the nodes required if asked). Returns success"""
    464          self._filename = filename
    465          filenode = self.getfilenode(filename)
    466          if filenode is None:
    467              if not createifmissing:
    468                  return False
    469              filenode = self.createfilenode(filename)
    470              self.document.getroot().append(filenode)
    471
    472          self.body = self.getbodynode(filenode, createifmissing=createifmissing)
    473          if self.body is None:
    474              return False
    475          self._messagenum = len(self.body.findall(".//%s" % self.namespaced("trans-unit")))
    476          #TODO: was 0 based before - consider
    477      #    messagenum = len(self.units)
    478          #TODO: we want to number them consecutively inside a body/file tag
    479          #instead of globally in the whole XLIFF file, but using len(self.units)
    480          #will be much faster
    481          return True
    482
    483 -    def creategroup(self, filename="NoName", createifmissing=False, restype=None):
    484          """adds a group tag into the specified file"""
    485          if self._filename != filename:
    486              if not self.switchfile(filename, createifmissing):
    487                  return None
    488          group = etree.SubElement(self.body, self.namespaced("group"))
    489          if restype:
    490              group.set("restype", restype)
    491          return group
    492
    493 -    def __str__(self):
    494          self.removedefaultfile()
    495          return super(xlifffile, self).__str__()
    496
    497 -    def parsestring(cls, storestring):
    498          """Parses the string to return the correct file object"""
    499          xliff = super(xlifffile, cls).parsestring(storestring)
    500          if xliff.units:
    501              header = xliff.units[0]
    502              if ("gettext-domain-header" in (header.getrestype() or "") \
    503                      or xliff.getdatatype() == "po") \
    504                      and cls.__name__.lower() != "poxlifffile":
    505                  import poxliff
    506                  xliff = poxliff.PoXliffFile.parsestring(storestring)
    507          return xliff
    508      parsestring = classmethod(parsestring)
    509