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-2010 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 XLIFF files for translation.
     22
     23  The official recommendation is to use the extention .xlf for XLIFF files.
     24  """
     25
     26  from lxml import etree
     27
     28  from translate.misc.multistring import multistring
     29  from translate.storage import base, lisa
     30  from translate.storage.lisa import getXMLspace
     31  from translate.storage.placeables.lisa import xml_to_strelem, strelem_to_xml
     32  from translate.storage.workflow import StateEnum as state
     33
     34  # TODO: handle translation types
     35
     36  ID_SEPARATOR = u"\04"
     37  # ID_SEPARATOR is commonly used through toolkit to generate compound
     38  # unit ids (for instance to concatenate msgctxt and msgid in po), but
     39  # \04 is an illegal char in XML 1.0, ID_SEPARATOR_SAFE will be used
     40  # instead when converting between xliff and other toolkit supported
     41  # formats
     42  ID_SEPARATOR_SAFE = u"__%04__"
     43
     44
     45 -class xliffunit(lisa.LISAunit):
     46      """A single term in the xliff file."""
     47
     48      rootNode = "trans-unit"
     49      languageNode = "source"
     50      textNode = ""
     51      namespace = 'urn:oasis:names:tc:xliff:document:1.1'
     52
     53      _default_xml_space = "default"
     54
     55      #TODO: id and all the trans-unit level stuff
     56
     57      S_UNTRANSLATED = state.EMPTY
     58      S_NEEDS_TRANSLATION = state.NEEDS_WORK
     59      S_NEEDS_REVIEW = state.NEEDS_REVIEW
     60      S_TRANSLATED = state.UNREVIEWED
     61      S_SIGNED_OFF = state.FINAL
     62      S_FINAL = state.MAX
     63
     64      statemap = {"new": S_UNTRANSLATED + 1,
     65                  "needs-translation": S_NEEDS_TRANSLATION,
     66                  "needs-adaptation": S_NEEDS_TRANSLATION + 1,
     67                  "needs-l10n": S_NEEDS_TRANSLATION + 2,
     68                  "needs-review-translation": S_NEEDS_REVIEW,
     69                  "needs-review-adaptation": S_NEEDS_REVIEW + 1,
     70                  "needs-review-l10n": S_NEEDS_REVIEW + 2,
     71                  "translated": S_TRANSLATED,
     72                  "signed-off": S_SIGNED_OFF,
     73                  "final": S_FINAL,
     74                  }
     75
     76      statemap_r = dict((i[1], i[0]) for i in statemap.iteritems())
     77
     78      STATE = {
     79          S_UNTRANSLATED: (state.EMPTY, state.NEEDS_WORK),
     80          S_NEEDS_TRANSLATION: (state.NEEDS_WORK, state.NEEDS_REVIEW),
     81          S_NEEDS_REVIEW: (state.NEEDS_REVIEW, state.UNREVIEWED),
     82          S_TRANSLATED: (state.UNREVIEWED, state.FINAL),
     83          S_SIGNED_OFF: (state.FINAL, state.MAX),
     84      }
     85
     86 -    def __init__(self, source, empty=False, **kwargs):
     87          """Override the constructor to set xml:space="preserve"."""
     88          super(xliffunit, self).__init__(source, empty, **kwargs)
     89          if empty:
     90              return
     91          lisa.setXMLspace(self.xmlelement, "preserve")
     92
     93 -    def createlanguageNode(self, lang, text, purpose):
     94          """Returns an xml Element setup with given parameters."""
     95
     96          #TODO: for now we do source, but we have to test if it is target, perhaps
     97          # with parameter. Alternatively, we can use lang, if supplied, since an xliff
     98          #file has to conform to the bilingual nature promised by the header.
     99          assert purpose
    100          langset = etree.Element(self.namespaced(purpose))
    101          #TODO: check language
    102  #        lisa.setXMLlang(langset, lang)
    103
    104  #        self.createPHnodes(langset, text)
    105          langset.text = text
    106          return langset
    107
    108 -    def getlanguageNodes(self):
    109          """We override this to get source and target nodes."""
    110          source = None
    111          target = None
    112          nodes = []
    113          try:
    114              source = self.xmlelement.iterchildren(self.namespaced(self.languageNode)).next()
    115              target = self.xmlelement.iterchildren(self.namespaced('target')).next()
    116              nodes = [source, target]
    117          except StopIteration:
    118              if source is not None:
    119                  nodes.append(source)
    120              if not target is None:
    121                  nodes.append(target)
    122          return nodes
    123
    124 -    def set_rich_source(self, value, sourcelang='en'):
    125          sourcelanguageNode = self.get_source_dom()
    126          if sourcelanguageNode is None:
    127              sourcelanguageNode = self.createlanguageNode(sourcelang, u'', "source")
    128              self.set_source_dom(sourcelanguageNode)
    129
    130          # Clear sourcelanguageNode first
    131          for i in range(len(sourcelanguageNode)):
    132              del sourcelanguageNode[0]
    133          sourcelanguageNode.text = None
    134
    135          strelem_to_xml(sourcelanguageNode, value[0])
    136
    137 -    def get_rich_source(self):
    138          #rsrc = xml_to_strelem(self.source_dom)
    139          #logging.debug('rich source: %s' % (repr(rsrc)))
    140          #from dubulib.debug.misc import print_stack_funcs
    141          #print_stack_funcs()
    142          return [xml_to_strelem(self.source_dom, getXMLspace(self.xmlelement, self._default_xml_space))]
    143      rich_source = property(get_rich_source, set_rich_source)
    144
    145 -    def set_rich_target(self, value, lang='xx', append=False):
    146          if value is None:
    147              self.set_target_dom(self.createlanguageNode(lang, u'', "target"))
    148              return
    149
    150          languageNode = self.get_target_dom()
    151          if languageNode is None:
    152              languageNode = self.createlanguageNode(lang, u'', "target")
    153              self.set_target_dom(languageNode, append)
    154
    155          # Clear languageNode first
    156          for i in range(len(languageNode)):
    157              del languageNode[0]
    158          languageNode.text = None
    159
    160          strelem_to_xml(languageNode, value[0])
    161
    162 -    def get_rich_target(self, lang=None):
    163          """retrieves the "target" text (second entry), or the entry in the
    164          specified language, if it exists"""
    165          return [xml_to_strelem(self.get_target_dom(lang), getXMLspace(self.xmlelement, self._default_xml_space))]
    166      rich_target = property(get_rich_target, set_rich_target)
    167
    168 -    def addalttrans(self, txt, origin=None, lang=None, sourcetxt=None, matchquality=None):
    169          """Adds an alt-trans tag and alt-trans components to the unit.
    170
    171          @type txt: String
    172          @param txt: Alternative translation of the source text.
    173          """
    174
    175          #TODO: support adding a source tag ad match quality attribute.  At
    176          # the source tag is needed to inject fuzzy matches from a TM.
    177          if isinstance(txt, str):
    178              txt = txt.decode("utf-8")
    179          alttrans = etree.SubElement(self.xmlelement, self.namespaced("alt-trans"))
    180          lisa.setXMLspace(alttrans, "preserve")
    181          if sourcetxt:
    182              if isinstance(sourcetxt, str):
    183                  sourcetxt = sourcetxt.decode("utf-8")
    184              altsource = etree.SubElement(alttrans, self.namespaced("source"))
    185              altsource.text = sourcetxt
    186          alttarget = etree.SubElement(alttrans, self.namespaced("target"))
    187          alttarget.text = txt
    188          if matchquality:
    189              alttrans.set("match-quality", matchquality)
    190          if origin:
    191              alttrans.set("origin", origin)
    192          if lang:
    193              lisa.setXMLlang(alttrans, lang)
    194
    195 -    def getalttrans(self, origin=None):
    196          """Returns <alt-trans> for the given origin as a list of units. No
    197          origin means all alternatives."""
    198          translist = []
    199          for node in self.xmlelement.iterdescendants(self.namespaced("alt-trans")):
    200              if self.correctorigin(node, origin):
    201                  # We build some mini units that keep the xmlelement. This
    202                  # makes it easier to delete it if it is passed back to us.
    203                  newunit = base.TranslationUnit(self.source)
    204
    205                  # the source tag is optional
    206                  sourcenode = node.iterdescendants(self.namespaced("source"))
    207                  try:
    208                      newunit.source = lisa.getText(sourcenode.next(), getXMLspace(node, self._default_xml_space))
    209                  except StopIteration:
    210                      pass
    211
    212                  # must have one or more targets
    213                  targetnode = node.iterdescendants(self.namespaced("target"))
    214                  newunit.target = lisa.getText(targetnode.next(), getXMLspace(node, self._default_xml_space))
    215                  #TODO: support multiple targets better
    216                  #TODO: support notes in alt-trans
    217                  newunit.xmlelement = node
    218
    219                  translist.append(newunit)
    220          return translist
    221
    222 -    def delalttrans(self, alternative):
    223          """Removes the supplied alternative from the list of alt-trans tags"""
    224          self.xmlelement.remove(alternative.xmlelement)
    225
    226 -    def addnote(self, text, origin=None, position="append"):
    227          """Add a note specifically in a "note" tag"""
    228          if position != "append":
    229              self.removenotes(origin=origin)
    230
    231          if text:
    232              text = text.strip()
    233          if not text:
    234              return
    235          if isinstance(text, str):
    236              text = text.decode("utf-8")
    237          note = etree.SubElement(self.xmlelement, self.namespaced("note"))
    238          note.text = text
    239          if origin:
    240              note.set("from", origin)
    241
    242 -    def getnotelist(self, origin=None):
    243          """Private method that returns the text from notes matching 'origin' or all notes."""
    244          notenodes = self.xmlelement.iterdescendants(self.namespaced("note"))
    245          # TODO: consider using xpath to construct initial_list directly
    246          # or to simply get the correct text from the outset (just remember to
    247          # check for duplication.
    248          initial_list = [lisa.getText(note, getXMLspace(self.xmlelement, self._default_xml_space)) for note in notenodes if self.correctorigin(note, origin)]
    249
    250          # Remove duplicate entries from list:
    251          dictset = {}
    252          notelist = [dictset.setdefault(note, note) for note in initial_list if note not in dictset]
    253
    254          return notelist
    255
    256 -    def getnotes(self, origin=None):
    257          return '\n'.join(self.getnotelist(origin=origin))
    258
    259 -    def removenotes(self, origin="translator"):
    260          """Remove all the translator notes."""
    261          notes = self.xmlelement.iterdescendants(self.namespaced("note"))
    262          for note in notes:
    263              if self.correctorigin(note, origin=origin):
    264                  self.xmlelement.remove(note)
    265
    266 -    def adderror(self, errorname, errortext):
    267          """Adds an error message to this unit."""
    268          #TODO: consider factoring out: some duplication between XLIFF and TMX
    269          text = errorname
    270          if errortext:
    271              text += ': ' + errortext
    272          self.addnote(text, origin="pofilter")
    273
    274 -    def geterrors(self):
    275          """Get all error messages."""
    276          #TODO: consider factoring out: some duplication between XLIFF and TMX
    277          notelist = self.getnotelist(origin="pofilter")
    278          errordict = {}
    279          for note in notelist:
    280              errorname, errortext = note.split(': ')
    281              errordict[errorname] = errortext
    282          return errordict
    283
    284 -    def get_state_n(self):
    285          targetnode = self.getlanguageNode(lang=None, index=1)
    286          if targetnode is None:
    287              if self.isapproved():
    288                  return self.S_UNREVIEWED
    289              elif self.target:
    290                  return self.S_NEEDS_TRANSLATION
    291              else:
    292                  return self.S_UNTRANSLATED
    293
    294          xmlstate = targetnode.get("state", None)
    295          state_n = self.statemap.get(xmlstate, self.S_UNTRANSLATED)
    296
    297          if state_n < self.S_NEEDS_TRANSLATION and self.target:
    298              state_n = self.S_NEEDS_TRANSLATION
    299
    300          if self.isapproved() and state_n < self.S_UNREVIEWED:
    301              state_n = self.S_UNREVIEWED
    302
    303          if not self.isapproved() and state_n > self.S_UNREVIEWED:
    304              state_n = self.S_UNREVIEWED
    305
    306          return  state_n
    307
    308 -    def set_state_n(self, value):
    309          if value not in self.statemap_r:
    310              value = self.get_state_id(value)
    311
    312          targetnode = self.getlanguageNode(lang=None, index=1)
    313
    314          #FIXME: handle state qualifiers
    315          if value == self.S_UNTRANSLATED:
    316              if targetnode is not None and state in targetnode.attrib:
    317                  del targetnode.attrib["state"]
    318          else:
    319              if targetnode is not None:
    320                  xmlstate = self.statemap_r.get(value)
    321                  targetnode.set("state", xmlstate)
    322
    323          self.markapproved(value > self.S_NEEDS_REVIEW)
    324
    325 -    def isapproved(self):
    326          """States whether this unit is approved."""
    327          return self.xmlelement.get("approved") == "yes"
    328
    329 -    def markapproved(self, value=True):
    330          """Mark this unit as approved."""
    331          if value:
    332              self.xmlelement.set("approved", "yes")
    333          elif self.isapproved():
    334              self.xmlelement.set("approved", "no")
    335
    336 -    def isreview(self):
    337          """States whether this unit needs to be reviewed"""
    338          return self.get_state_id() == self.S_NEEDS_REVIEW
    339
    340 -    def markreviewneeded(self, needsreview=True, explanation=None):
    341          """Marks the unit to indicate whether it needs review. Adds an optional explanation as a note."""
    342          state_id = self.get_state_id()
    343          if needsreview and state_id != self.S_NEEDS_REVIEW:
    344              self.set_state_n(self.S_NEEDS_REVIEW)
    345              if explanation:
    346                  self.addnote(explanation, origin="translator")
    347          elif not needsreview and state_id < self.S_UNREVIEWED:
    348              self.set_state_n(self.S_UNREVIEWED)
    349
    350 -    def isfuzzy(self):
    351  #        targetnode = self.getlanguageNode(lang=None, index=1)
    352  #        return not targetnode is None and \
    353  #                (targetnode.get("state-qualifier") == "fuzzy-match" or \
    354  #                targetnode.get("state") == "needs-review-translation")
    355          return not self.isapproved()
    356
    357 -    def markfuzzy(self, value=True):
    358          state_id = self.get_state_id()
    359          if value:
    360              self.markapproved(False)
    361              if state_id != self.S_NEEDS_TRANSLATION:
    362                  self.set_state_n(self.S_NEEDS_TRANSLATION)
    363          else:
    364              self.markapproved(True)
    365              if state_id < self.S_UNREVIEWED:
    366                  self.set_state_n(self.S_UNREVIEWED)
    367
    368 -    def settarget(self, text, lang='xx', append=False):
    369          """Sets the target string to the given value."""
    370          super(xliffunit, self).settarget(text, lang, append)
    371          if text:
    372              self.marktranslated()
    373
    374  # This code is commented while this will almost always return false.
    375  # This way pocount, etc. works well.
    376  #    def istranslated(self):
    377  #        targetnode = self.getlanguageNode(lang=None, index=1)
    378  #        return not targetnode is None and \
    379  #                (targetnode.get("state") == "translated")
    380
    381 -    def istranslatable(self):
    382          value = self.xmlelement.get("translate")
    383          if value and value.lower() == 'no':
    384              return False
    385          return True
    386
    387 -    def marktranslated(self):
    388          state_id = self.get_state_id()
    389          if state_id < self.S_UNREVIEWED:
    390              self.set_state_n(self.S_UNREVIEWED)
    391
    392 -    def setid(self, id):
    393          # sanitize id in case ID_SEPERATOR is present
    394          self.xmlelement.set("id", id.replace(ID_SEPARATOR, ID_SEPARATOR_SAFE))
    395
    396 -    def getid(self):
    397          uid = u""
    398          try:
    399              filename = self.xmlelement.iterancestors(self.namespaced('file')).next().get('original')
    400              if filename:
    401                  uid = filename + ID_SEPARATOR
    402          except StopIteration:
    403              # unit has no proper file ancestor, probably newly created
    404              pass
    405          # hide the fact that we sanitize ID_SEPERATOR
    406          uid += unicode(self.xmlelement.get("id") or u"").replace(ID_SEPARATOR_SAFE, ID_SEPARATOR)
    407          return uid
    408
    409 -    def addlocation(self, location):
    410          self.setid(location)
    411
    412 -    def getlocations(self):
    413          id_attr = unicode(self.xmlelement.get("id") or u"")
    414          if id_attr:
    415              return [id_attr]
    416          return []
    417
    418 -    def createcontextgroup(self, name, contexts=None, purpose=None):
    419          """Add the context group to the trans-unit with contexts a list with
    420          (type, text) tuples describing each context."""
    421          assert contexts
    422          group = etree.Element(self.namespaced("context-group"))
    423          # context-group tags must appear at the start within <group>
    424          # tags. Otherwise it must be appended to the end of a group
    425          # of tags.
    426          if self.xmlelement.tag == self.namespaced("group"):
    427              self.xmlelement.insert(0, group)
    428          else:
    429              self.xmlelement.append(group)
    430          group.set("name", name)
    431          if purpose:
    432              group.set("purpose", purpose)
    433          for type, text in contexts:
    434              if isinstance(text, str):
    435                  text = text.decode("utf-8")
    436              context = etree.SubElement(group, self.namespaced("context"))
    437              context.text = text
    438              context.set("context-type", type)
    439
    440 -    def getcontextgroups(self, name):
    441          """Returns the contexts in the context groups with the specified name"""
    442          groups = []
    443          grouptags = self.xmlelement.iterdescendants(self.namespaced("context-group"))
    444          #TODO: conbine name in query
    445          for group in grouptags:
    446              if group.get("name") == name:
    447                  contexts = group.iterdescendants(self.namespaced("context"))
    448                  pairs = []
    449                  for context in contexts:
    450                      pairs.append((context.get("context-type"), lisa.getText(context, getXMLspace(self.xmlelement, self._default_xml_space))))
    451                  groups.append(pairs) #not extend
    452          return groups
    453
    454 -    def getrestype(self):
    455          """returns the restype attribute in the trans-unit tag"""
    456          return self.xmlelement.get("restype")
    457
    458 -    def merge(self, otherunit, overwrite=False, comments=True, authoritative=False):
    459          #TODO: consider other attributes like "approved"
    460          super(xliffunit, self).merge(otherunit, overwrite, comments)
    461          if self.target:
    462              self.marktranslated()
    463              if otherunit.isfuzzy():
    464                  self.markfuzzy()
    465              elif otherunit.source == self.source:
    466                  self.markfuzzy(False)
    467          if comments:
    468              self.addnote(otherunit.getnotes())
    469
    470 -    def correctorigin(self, node, origin):
    471          """Check against node tag's origin (e.g note or alt-trans)"""
    472          if origin == None:
    473              return True
    474          elif origin in node.get("from", ""):
    475              return True
    476          elif origin in node.get("origin", ""):
    477              return True
    478          else:
    479              return False
    480
    481 -    def multistring_to_rich(cls, mstr):
    482          """Override L{TranslationUnit.multistring_to_rich} which is used by the
    483              C{rich_source} and C{rich_target} properties."""
    484          strings = mstr
    485          if isinstance(mstr, multistring):
    486              strings = mstr.strings
    487          elif isinstance(mstr, basestring):
    488              strings = [mstr]
    489
    490          return [xml_to_strelem(s) for s in strings]
    491      multistring_to_rich = classmethod(multistring_to_rich)
    492
    493 -    def rich_to_multistring(cls, elem_list):
    494          """Override L{TranslationUnit.rich_to_multistring} which is used by the
    495              C{rich_source} and C{rich_target} properties."""
    496          return multistring([unicode(elem) for elem in elem_list])
    497      rich_to_multistring = classmethod(rich_to_multistring)
    498
    499
    500 -class xlifffile(lisa.LISAfile):
    501      """Class representing a XLIFF file store."""
    502      UnitClass = xliffunit
    503      Name = _("XLIFF Translation File")
    504      Mimetypes = ["application/x-xliff", "application/x-xliff+xml"]
    505      Extensions = ["xlf", "xliff", "sdlxliff"]
    506      rootNode = "xliff"
    507      bodyNode = "body"
    508      XMLskeleton = '''<?xml version="1.0" ?>
    509  <xliff version='1.1' xmlns='urn:oasis:names:tc:xliff:document:1.1'>
    510  <file original='NoName' source-language='en' datatype='plaintext'>
    511  <body>
    512  </body>
    513  </file>
    514  </xliff>'''
    515      namespace = 'urn:oasis:names:tc:xliff:document:1.1'
    516      suggestions_in_format = True
    517      """xliff units have alttrans tags which can be used to store suggestions"""
    518
    519 -    def __init__(self, *args, **kwargs):
    520          self._filename = None
    521          lisa.LISAfile.__init__(self, *args, **kwargs)
    522          self._messagenum = 0
    523
    524 -    def initbody(self):
    525          self.namespace = self.document.getroot().nsmap.get(None, None)
    526
    527          if self._filename:
    528              filenode = self.getfilenode(self._filename, createifmissing=True)
    529          else:
    530              filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
    531          self.body = self.getbodynode(filenode, createifmissing=True)
    532
    533 -    def addheader(self):
    534          """Initialise the file header."""
    535          pass
    536
    537 -    def createfilenode(self, filename, sourcelanguage=None, targetlanguage=None, datatype='plaintext'):
    538          """creates a filenode with the given filename. All parameters
    539          are needed for XLIFF compliance."""
    540          if sourcelanguage is None:
    541              sourcelanguage = self.sourcelanguage
    542          if targetlanguage is None:
    543              targetlanguage = self.targetlanguage
    544
    545          # find the default NoName file tag and use it instead of creating a new one
    546          for filenode in self.document.getroot().iterchildren(self.namespaced("file")):
    547              if filenode.get("original") == "NoName":
    548                  filenode.set("original", filename)
    549                  filenode.set("source-language", sourcelanguage)
    550                  if targetlanguage:
    551                      filenode.set("target-language", targetlanguage)
    552                  return filenode
    553
    554          filenode = etree.Element(self.namespaced("file"))
    555          filenode.set("original", filename)
    556          filenode.set("source-language", sourcelanguage)
    557          if targetlanguage:
    558              filenode.set("target-language", targetlanguage)
    559          filenode.set("datatype", datatype)
    560          bodyNode = etree.SubElement(filenode, self.namespaced(self.bodyNode))
    561          return filenode
    562
    563 -    def getfilename(self, filenode):
    564          """returns the name of the given file"""
    565          return filenode.get("original")
    566
    567 -    def setfilename(self, filenode, filename):
    568          """set the name of the given file"""
    569          return filenode.set("original", filename)
    570
    571 -    def getfilenames(self):
    572          """returns all filenames in this XLIFF file"""
    573          filenodes = self.document.getroot().iterchildren(self.namespaced("file"))
    574          filenames = [self.getfilename(filenode) for filenode in filenodes]
    575          filenames = filter(None, filenames)
    576          if len(filenames) == 1 and filenames[0] == '':
    577              filenames = []
    578          return filenames
    579
    580 -    def getfilenode(self, filename, createifmissing=False):
    581          """finds the filenode with the given name"""
    582          filenodes = self.document.getroot().iterchildren(self.namespaced("file"))
    583          for filenode in filenodes:
    584              if self.getfilename(filenode) == filename:
    585                  return filenode
    586          if createifmissing:
    587              filenode = self.createfilenode(filename)
    588              return filenode
    589          return None
    590
    591 -    def getids(self, filename=None):
    592          if not filename:
    593              return super(xlifffile, self).getids()
    594
    595          self.id_index = {}
    596          prefix = filename + ID_SEPARATOR
    597          units = (unit for unit in self.units if unit.getid().startswith(prefix))
    598          for index, unit in enumerate(units):
    599              self.id_index[unit.getid()[len(prefix):]] = unit
    600          return self.id_index.keys()
    601
    602 -    def setsourcelanguage(self, language):
    603          if not language:
    604              return
    605          filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
    606          filenode.set("source-language", language)
    607
    608 -    def getsourcelanguage(self):
    609          filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
    610          return filenode.get("source-language")
    611      sourcelanguage = property(getsourcelanguage, setsourcelanguage)
    612
    613 -    def settargetlanguage(self, language):
    614          if not language:
    615              return
    616          filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
    617          filenode.set("target-language", language)
    618
    619 -    def gettargetlanguage(self):
    620          filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
    621          return filenode.get("target-language")
    622      targetlanguage = property(gettargetlanguage, settargetlanguage)
    623
    624 -    def getdatatype(self, filename=None):
    625          """Returns the datatype of the stored file. If no filename is given,
    626          the datatype of the first file is given."""
    627          if filename:
    628              node = self.getfilenode(filename)
    629              if not node is None:
    630                  return node.get("datatype")
    631          else:
    632              filenames = self.getfilenames()
    633              if len(filenames) > 0 and filenames[0] != "NoName":
    634                  return self.getdatatype(filenames[0])
    635          return ""
    636
    637 -    def getdate(self, filename=None):
    638          """Returns the date attribute for the file. If no filename is given,
    639          the date of the first file is given. If the date attribute is not
    640          specified, None is returned."""
    641          if filename:
    642              node = self.getfilenode(filename)
    643              if not node is None:
    644                  return node.get("date")
    645          else:
    646              filenames = self.getfilenames()
    647              if len(filenames) > 0 and filenames[0] != "NoName":
    648                  return self.getdate(filenames[0])
    649          return None
    650
    651 -    def removedefaultfile(self):
    652          """We want to remove the default file-tag as soon as possible if we
    653          know if still present and empty."""
    654          filenodes = list(self.document.getroot().iterchildren(self.namespaced("file")))
    655          if len(filenodes) > 1:
    656              for filenode in filenodes:
    657                  if filenode.get("original") == "NoName" and \
    658                          not list(filenode.iterdescendants(self.namespaced(self.UnitClass.rootNode))):
    659                      self.document.getroot().remove(filenode)
    660                  break
    661
    662 -    def getheadernode(self, filenode, createifmissing=False):
    663          """finds the header node for the given filenode"""
    664          # TODO: Deprecated?
    665          headernode = filenode.iterchildren(self.namespaced("header"))
    666          try:
    667              return headernode.next()
    668          except StopIteration:
    669              pass
    670          if not createifmissing:
    671              return None
    672          headernode = etree.SubElement(filenode, self.namespaced("header"))
    673          return headernode
    674
    675 -    def getbodynode(self, filenode, createifmissing=False):
    676          """finds the body node for the given filenode"""
    677          bodynode = filenode.iterchildren(self.namespaced("body"))
    678          try:
    679              return bodynode.next()
    680          except StopIteration:
    681              pass
    682          if not createifmissing:
    683              return None
    684          bodynode = etree.SubElement(filenode, self.namespaced("body"))
    685          return bodynode
    686
    687 -    def addsourceunit(self, source, filename="NoName", createifmissing=False):
    688          """adds the given trans-unit to the last used body node if the
    689          filename has changed it uses the slow method instead (will
    690          create the nodes required if asked). Returns success"""
    691          if self._filename != filename:
    692              if not self.switchfile(filename, createifmissing):
    693                  return None
    694          unit = super(xlifffile, self).addsourceunit(source)
    695          self._messagenum += 1
    696          unit.setid("%d" % self._messagenum)
    697          return unit
    698
    699 -    def switchfile(self, filename, createifmissing=False):
    700          """adds the given trans-unit (will create the nodes required if asked). Returns success"""
    701          self._filename = filename
    702          filenode = self.getfilenode(filename)
    703          if filenode is None:
    704              if not createifmissing:
    705                  return False
    706              filenode = self.createfilenode(filename)
    707              self.document.getroot().append(filenode)
    708
    709          self.body = self.getbodynode(filenode, createifmissing=createifmissing)
    710          if self.body is None:
    711              return False
    712          self._messagenum = len(list(self.body.iterdescendants(self.namespaced("trans-unit"))))
    713          #TODO: was 0 based before - consider
    714      #    messagenum = len(self.units)
    715          #TODO: we want to number them consecutively inside a body/file tag
    716          #instead of globally in the whole XLIFF file, but using len(self.units)
    717          #will be much faster
    718          return True
    719
    720 -    def creategroup(self, filename="NoName", createifmissing=False, restype=None):
    721          """adds a group tag into the specified file"""
    722          if self._filename != filename:
    723              if not self.switchfile(filename, createifmissing):
    724                  return None
    725          group = etree.SubElement(self.body, self.namespaced("group"))
    726          if restype:
    727              group.set("restype", restype)
    728          return group
    729
    730 -    def __str__(self):
    731          self.removedefaultfile()
    732          return super(xlifffile, self).__str__()
    733
    734 -    def parsestring(cls, storestring):
    735          """Parses the string to return the correct file object"""
    736          xliff = super(xlifffile, cls).parsestring(storestring)
    737          if xliff.units:
    738              header = xliff.units[0]
    739              if ("gettext-domain-header" in (header.getrestype() or "") \
    740                      or xliff.getdatatype() == "po") \
    741                      and cls.__name__.lower() != "poxlifffile":
    742                  from translate.storage import poxliff
    743                  xliff = poxliff.PoXliffFile.parsestring(storestring)
    744          return xliff
    745      parsestring = classmethod(parsestring)
    746