translate.storage.poheader

Package translate :: Package storage :: Module poheader |
[hide private]
[frames] | no frames]

# Source Code for Module translate.storage.poheader

      1  #!/usr/bin/env python
      2  # -*- coding: utf-8 -*-
      3  #
      4  # Copyright 2002-2011 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  """class that handles all header functions for a header in a po file"""
     22
     23  import re
     24  import time
     25
     26  from translate import __version__
     27  from translate.misc import dictutils
     28
     29  author_re = re.compile(r".*<\S+@\S+>.*\d{4,4}")
     30
     31  default_header = {
     32      "Project-Id-Version": "PACKAGE VERSION",
     33      "PO-Revision-Date": "YEAR-MO-DA HO:MI+ZONE",
     34      "Last-Translator": "FULL NAME <EMAIL@ADDRESS>",
     35      "Language-Team": "LANGUAGE <LL@li.org>",
     36      "Plural-Forms": "nplurals=INTEGER; plural=EXPRESSION;",
     37      }
     38
     39
     40 -def parseheaderstring(input):
     41      """Parses an input string with the definition of a PO header and returns
     42      the interpreted values as a dictionary."""
     43      headervalues = dictutils.ordereddict()
     44      for line in input.split("\n"):
     45          if not line or ":" not in line:
     46              continue
     47          key, value = line.split(":", 1)
     48          #We don't want unicode keys
     49          key = str(key.strip())
     50          headervalues[key] = value.strip()
     51      return headervalues
     52
     53
     54 -def tzstring():
     55      """Returns the timezone as a string in the format [+-]0000, eg +0200.
     56
     57      @rtype: str"""
     58      if time.daylight:
     59          tzoffset = time.altzone
     60      else:
     61          tzoffset = time.timezone
     62
     63      hours, minutes = time.gmtime(abs(tzoffset))[3:5]
     64      if tzoffset > 0:
     65          hours *= -1
     66      tz = str("%+d" % hours).zfill(3) + str(minutes).zfill(2)
     67      return tz
     68
     69
     70 -def update(existing, add=False, **kwargs):
     71      """Update an existing header dictionary with the values in kwargs, adding new values
     72      only if add is true.
     73
     74      @return: Updated dictionary of header entries
     75      @rtype: dict
     76      """
     77      headerargs = dictutils.ordereddict()
     78      fixedargs = dictutils.cidict()
     79      for key, value in kwargs.items():
     80          key = key.replace("_", "-")
     81          if key.islower():
     82              key = key.title()
     83          fixedargs[key] = value
     84      removed = []
     85      for key in poheader.header_order:
     86          if key in existing:
     87              if key in fixedargs:
     88                  headerargs[key] = fixedargs.pop(key)
     89              else:
     90                  headerargs[key] = existing[key]
     91              removed.append(key)
     92          elif add and key in fixedargs:
     93              headerargs[key] = fixedargs.pop(key)
     94      for key, value in existing.iteritems():
     95          if not key in removed:
     96              headerargs[key] = value
     97      if add:
     98          for key in fixedargs:
     99              headerargs[key] = fixedargs[key]
    100      return headerargs
    101
    102
    103 -class poheader(object):
    104      """This class implements functionality for manipulation of po file headers.
    105      This class is a mix-in class and useless on its own. It must be used from all
    106      classes which represent a po file"""
    107
    108      x_generator = "Translate Toolkit %s" % __version__.sver
    109
    110      header_order = [
    111          "Project-Id-Version",
    112          "Report-Msgid-Bugs-To",
    113          "POT-Creation-Date",
    114          "PO-Revision-Date",
    115          "Last-Translator",
    116          "Language-Team",
    117          "Language",
    118          "MIME-Version",
    119          "Content-Type",
    120          "Content-Transfer-Encoding",
    121          "Plural-Forms",
    122          "X-Generator",
    123          ]
    124
    125 -    def init_headers(self, charset='utf-8', encoding='8bit', **kwargs):
    126          """sets default values for po headers"""
    127          #FIXME: we need to allow at least setting target language, pluralforms and generator
    128          headerdict = self.makeheaderdict(charset=charset, encoding=encoding, **kwargs)
    129          self.updateheader(add=True, **headerdict)
    130          return self.header()
    131
    132 -    def makeheaderdict(self,
    133              charset="CHARSET",
    134              encoding="ENCODING",
    135              project_id_version=None,
    136              pot_creation_date=None,
    137              po_revision_date=None,
    138              last_translator=None,
    139              language_team=None,
    140              mime_version=None,
    141              plural_forms=None,
    142              report_msgid_bugs_to=None,
    143              **kwargs):
    144          """Create a header dictionary with useful defaults.
    145
    146          pot_creation_date can be None (current date) or a value (datetime or string)
    147          po_revision_date can be None (form), False (=pot_creation_date), True (=now),
    148          or a value (datetime or string)
    149
    150          @return: Dictionary with the header items
    151          @rtype: dict
    152          """
    153          if project_id_version is None:
    154              project_id_version = "PACKAGE VERSION"
    155          if pot_creation_date is None or pot_creation_date == True:
    156              pot_creation_date = time.strftime("%Y-%m-%d %H:%M") + tzstring()
    157          if isinstance(pot_creation_date, time.struct_time):
    158              pot_creation_date = time.strftime("%Y-%m-%d %H:%M", pot_creation_date) + tzstring()
    159          if po_revision_date is None:
    160              po_revision_date = "YEAR-MO-DA HO:MI+ZONE"
    161          elif po_revision_date == False:
    162              po_revision_date = pot_creation_date
    163          elif po_revision_date == True:
    164              po_revision_date = time.strftime("%Y-%m-%d %H:%M") + tzstring()
    165          if isinstance(po_revision_date, time.struct_time):
    166              po_revision_date = time.strftime("%Y-%m-%d %H:%M", po_revision_date) + tzstring()
    167          if last_translator is None:
    168              last_translator = "FULL NAME <EMAIL@ADDRESS>"
    169          if language_team is None:
    170              language_team = "LANGUAGE <LL@li.org>"
    171          if mime_version is None:
    172              mime_version = "1.0"
    173          if report_msgid_bugs_to is None:
    174              report_msgid_bugs_to = ""
    175
    176          defaultargs = dictutils.ordereddict()
    177          defaultargs["Project-Id-Version"] = project_id_version
    178          defaultargs["Report-Msgid-Bugs-To"] = report_msgid_bugs_to
    179          defaultargs["POT-Creation-Date"] = pot_creation_date
    180          defaultargs["PO-Revision-Date"] = po_revision_date
    181          defaultargs["Last-Translator"] = last_translator
    182          defaultargs["Language-Team"] = language_team
    183          defaultargs["MIME-Version"] = mime_version
    184          defaultargs["Content-Type"] = "text/plain; charset=%s" % charset
    185          defaultargs["Content-Transfer-Encoding"] = encoding
    186          if plural_forms:
    187              defaultargs["Plural-Forms"] = plural_forms
    188          defaultargs["X-Generator"] = self.x_generator
    189
    190          return update(defaultargs, add=True, **kwargs)
    191
    192 -    def header(self):
    193          """Returns the header element, or None. Only the first element is allowed
    194          to be a header. Note that this could still return an empty header element,
    195          if present."""
    196          if len(self.units) == 0:
    197              return None
    198          candidate = self.units[0]
    199          if candidate.isheader():
    200              return candidate
    201          else:
    202              return None
    203
    204 -    def parseheader(self):
    205          """Parses the PO header and returns the interpreted values as a
    206          dictionary."""
    207          header = self.header()
    208          if not header:
    209              return {}
    210          return parseheaderstring(header.target)
    211
    212 -    def updateheader(self, add=False, **kwargs):
    213          """Updates the fields in the PO style header.
    214
    215          This will create a header if add == True."""
    216          header = self.header()
    217          if not header:
    218              if add:
    219                  header = self.makeheader(**kwargs)
    220                  self._insert_header(header)
    221          else:
    222              headeritems = update(self.parseheader(), add, **kwargs)
    223              keys = headeritems.keys()
    224              if not "Content-Type" in keys or "charset=CHARSET" in headeritems["Content-Type"]:
    225                  headeritems["Content-Type"] = "text/plain; charset=UTF-8"
    226              if not "Content-Transfer-Encoding" in keys or "ENCODING" in headeritems["Content-Transfer-Encoding"]:
    227                  headeritems["Content-Transfer-Encoding"] = "8bit"
    228              headerString = ""
    229              for key, value in headeritems.items():
    230                  if value is not None:
    231                      headerString += "%s: %s\n" % (key, value)
    232              header.target = headerString
    233              header.markfuzzy(False)    # TODO: check why we do this?
    234          return header
    235
    236 -    def _insert_header(self, header):
    237          # we should be using .addunit() or some equivalent in case the
    238          # unit needs to refer back to the store, etc. This might be
    239          # subtly broken for POXLIFF, since we don't dupliate the code
    240          # from lisa::addunit().
    241          header._store = self
    242          self.units.insert(0, header)
    243
    244 -    def getheaderplural(self):
    245          """Returns the nplural and plural values from the header."""
    246          header = self.parseheader()
    247          pluralformvalue = header.get('Plural-Forms', None)
    248          if pluralformvalue is None:
    249              return None, None
    250          nplural = re.findall("nplurals=(.+?);", pluralformvalue)
    251          plural = re.findall("plural=(.+?);?$", pluralformvalue)
    252          if not nplural or nplural[0] == "INTEGER":
    253              nplural = None
    254          else:
    255              nplural = nplural[0]
    256          if not plural or plural[0] == "EXPRESSION":
    257              plural = None
    258          else:
    259              plural = plural[0]
    260          return nplural, plural
    261
    262 -    def updateheaderplural(self, nplurals, plural):
    263          """Update the Plural-Form PO header."""
    264          if isinstance(nplurals, basestring):
    265              nplurals = int(nplurals)
    266          self.updateheader(add=True, Plural_Forms="nplurals=%d; plural=%s;" % (nplurals, plural))
    267
    268 -    def gettargetlanguage(self):
    269          """Return the target language based on information in the header.
    270
    271          The target language is determined in the following sequence:
    272            1. Use the 'Language' entry in the header.
    273            2. Poedit's custom headers.
    274            3. Analysing the 'Language-Team' entry.
    275          """
    276          header = self.parseheader()
    277          lang = header.get('Language', None)
    278          if lang is not None:
    279              from translate.lang.data import langcode_ire
    280              if langcode_ire.match(lang):
    281                  return lang
    282              else:
    283                  lang = None
    284          if 'X-Poedit-Language' in header:
    285              from translate.lang import poedit
    286              language = header.get('X-Poedit-Language')
    287              country = header.get('X-Poedit-Country')
    288              return poedit.isocode(language, country)
    289          if 'Language-Code' in header: # Used in Plone files
    290              return header.get('Language-Code')
    291          if 'Language-Team' in header:
    292              from translate.lang.team import guess_language
    293              return guess_language(header.get('Language-Team'))
    294          return None
    295
    296 -    def settargetlanguage(self, lang):
    297          """Set the target language in the header.
    298
    299          This removes any custom Poedit headers if they exist.
    300
    301          @param lang: the new target language code
    302          @type lang: str
    303          """
    304          if isinstance(lang, basestring) and len(lang) > 1:
    305              self.updateheader(add=True, Language=lang, X_Poedit_Language=None, X_Poedit_Country=None)
    306
    307 -    def getprojectstyle(self):
    308          """Return the project based on information in the header.
    309
    310          The project is determined in the following sequence:
    311            1. Use the 'X-Project-Style' entry in the header.
    312            2. Use 'Report-Msgid-Bug-To' entry
    313            3. Use the 'X-Accelerator' entry
    314            4. Use the Project ID
    315            5. Analyse the file itself (not yet implemented)
    316          """
    317          header = self.parseheader()
    318          project = header.get('X-Project-Style', None)
    319          if project is not None:
    320              return project
    321          bug_address = header.get('Report-Msgid-Bugs-To', None)
    322          if bug_address is not None:
    323             if 'bugzilla.gnome.org' in bug_address:
    324                 return 'gnome'
    325             if 'bugs.kde.org' in bug_address:
    326                 return 'kde'
    327          accelerator = header.get('X-Accelerator-Marker', None)
    328          if accelerator is not None:
    329              if accelerator == "~":
    330                  return "openoffice"
    331              elif accelerator == "&":
    332                  return "mozilla"
    333          project_id = header.get('Project-Id-Version', None)
    334          if project_id is not None:
    335              if 'gnome' in project_id.lower():
    336                  return "gnome"
    337          # TODO Call some project guessing code and probably move all of the above there also
    338          return None
    339
    340 -    def setprojectstyle(self, project_style):
    341          """Set the project in the header.
    342
    343          @param project_style: the new project
    344          @type project_style: str
    345          """
    346          from translate.filters.checks import projectcheckers
    347          if project_style in projectcheckers:
    348              self.updateheader(add=True, X_Project_Style=project_style)
    349
    350 -    def mergeheaders(self, otherstore):
    351          """Merges another header with this header.
    352
    353          This header is assumed to be the template.
    354
    355          @type otherstore: L{base.TranslationStore}
    356          """
    357
    358          newvalues = otherstore.parseheader()
    359          retain_list = ("Project-Id-Version", "PO-Revision-Date", "Last-Translator",
    360                         "Language-Team", "Plural-Forms")
    361          retain = dict((key, newvalues[key]) for key in retain_list if newvalues.get(key, None) and newvalues[key] != default_header.get(key, None))
    362          self.updateheader(**retain)
    363
    364 -    def updatecontributor(self, name, email=None):
    365          """Add contribution comments if necessary."""
    366          header = self.header()
    367          if not header:
    368              return
    369          prelines = []
    370          contriblines = []
    371          postlines = []
    372          contribexists = False
    373          incontrib = False
    374          outcontrib = False
    375          for line in header.getnotes("translator").split('\n'):
    376              line = line.strip()
    377              if line == u"FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.":
    378                  incontrib = True
    379                  continue
    380              if author_re.match(line):
    381                  incontrib = True
    382                  contriblines.append(line)
    383                  continue
    384              if line == "" and incontrib:
    385                  incontrib = False
    386                  outcontrib = True
    387              if incontrib:
    388                  contriblines.append(line)
    389              elif not outcontrib:
    390                  prelines.append(line)
    391              else:
    392                  postlines.append(line)
    393
    394          year = time.strftime("%Y")
    395          contribexists = False
    396          for i in range(len(contriblines)):
    397              line = contriblines[i]
    398              if name in line and (email is None or email in line):
    399                  contribexists = True
    400                  if year in line:
    401                      break
    402                  else:
    403                      #The contributor is there, but not for this year
    404                      if line[-1] == '.':
    405                          line = line[:-1]
    406                      contriblines[i] = "%s, %s." % (line, year)
    407
    408          if not contribexists:
    409              # Add a new contributor
    410              if email:
    411                  contriblines.append("%s <%s>, %s." % (name, email, year))
    412              else:
    413                  contriblines.append("%s, %s." % (name, year))
    414
    415          header.removenotes()
    416          header.addnote("\n".join(prelines))
    417          header.addnote("\n".join(contriblines))
    418          header.addnote("\n".join(postlines))
    419
    420 -    def makeheader(self, **kwargs):
    421          """Create a header for the given filename.
    422
    423          Check .makeheaderdict() for information on parameters."""
    424          headerpo = self.UnitClass("", encoding=self._encoding)
    425          headerpo.markfuzzy()
    426          headeritems = self.makeheaderdict(**kwargs)
    427          headervalue = ""
    428          for (key, value) in headeritems.items():
    429              if value is None:
    430                  continue
    431              headervalue += "%s: %s\n" % (key, value)
    432          headerpo.target = headervalue
    433          return headerpo
    434