Package translate :: Package storage :: Module properties

[hide private]
[frames] | no frames]

# Source Code for Module translate.storage.properties

      1  #!/usr/bin/env python
      2  # -*- coding: utf-8 -*-
      3  #
      4  # Copyright 2004-2006 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  """classes that hold units of .properties files (propunit) or entire files
     23     (propfile) these files are used in translating Mozilla and other software
     24
     25     The following U{.properties file
     26     description<http://java.sun.com/j2se/1.4.2/docs/api/java/util/Properties.html#load(java.io.InputStream)>}
     27     and U{example <http://www.exampledepot.com/egs/java.util/Props.html>} give some
     28     good references to the .properties specification.
     29
     30     Properties file may also hold Java
     31     U{MessageFormat<http://java.sun.com/j2se/1.4.2/docs/api/java/text/MessageFormat.html>}
     32     messages.  No special handling is provided in this storage class for MessageFormat,
     33     but this may be implemented in future.
     34
     35     Implementation
     36     ==============
     37     A simple summary of what is permissible follows.
     38
     39     Comments::
     40       # a comment
     41       ! a comment
     42
     43     Name and Value pairs::
     44       # Note that the b and c are escaped for epydoc rendering
     45       a = a string
     46       d.e.f = another string
     47       b = a string with escape sequences \\t \\n \\r \\\\ \\" \\' \\ (space) \u0123
     48       c = a string with a continuation line \\
     49           continuation line
     50  """
     51
     52  from translate.storage import base
     53  from translate.misc import quote
     54  from translate.lang import data
     55  import re
     56
     57  # the rstripeols convert dos <-> unix nicely as well
     58  # output will be appropriate for the platform
     59
     60  eol = "\n"
     61
     62 -def find_delimiter(line):
     63      """Find the type and position of the delimiter in a property line.
     64
     65      Property files can be delimeted by "=", ":" or whitespace (space for now).
     66      We find the position of each delimiter, then find the one that appears
     67      first.
     68
     69      @param line: A properties line
     70      @type line: str
     71      @return: delimiter character and offset within L{line}
     72      @rtype: Tuple (delimiter char, Offset Integer)
     73      """
     74      delimiters = {"=": -1, ":": -1, " ": -1}
     75      # Find the position of each delimiter type
     76      for delimiter, pos in delimiters.iteritems():
     77          prewhitespace = len(line) - len(line.lstrip())
     78          pos = line.find(delimiter, prewhitespace)
     79          while pos != -1:
     80              if delimiters[delimiter] == -1 and line[pos-1] != "\\":
     81                  delimiters[delimiter] = pos
     82                  break
     83              pos = line.find(delimiter, pos+1)
     84      # Find the first "=" or ":" delimiter
     85      mindelimiter = None
     86      minpos = -1
     87      for delimiter, pos in delimiters.iteritems():
     88          if pos == -1 or delimiter == " ":
     89              continue
     90          if minpos == -1 or pos < minpos:
     91              minpos = pos
     92              mindelimiter = delimiter
     93      if mindelimiter is None and delimiters[" "] != -1:
     94          # Use space delimiter if we found nothing else
     95          return (" ", delimiters[" "])
     96      if mindelimiter is not None and delimiters[" "] < delimiters[mindelimiter]:
     97          # If space delimiter occurs earlier then ":" or "=" then it is the
     98          # delimiter only if there are non-whitespace characters between it and
     99          # the other detected delimiter.
    100          if len(line[delimiters[" "]:delimiters[mindelimiter]].strip()) > 0:
    101              return (" ", delimiters[" "])
    102      return (mindelimiter, minpos)
    103
    104 -def find_delimeter(line):
    105      """Spelling error that is kept around for in case someone relies on it.
    106
    107      Deprecated."""
    108      raise DeprecationWarning
    109      return find_delimiter(line)
    110
    111 -def is_line_continuation(line):
    112      """Determine whether L{line} has a line continuation marker.
    113
    114      .properties files can be terminated with a backslash (\\) indicating
    115      that the 'value' continues on the next line.  Continuation is only
    116      valid if there are an odd number of backslashses (an even number
    117      would result in a set of N/2 slashes not an escape)
    118
    119      @param line: A properties line
    120      @type line: str
    121      @return: Does L{line} end with a line continuation
    122      @rtype: Boolean
    123      """
    124      pos = -1
    125      count = 0
    126      if len(line) == 0:
    127          return False
    128      # Count the slashes from the end of the line. Ensure we don't
    129      # go into infinite loop.
    130      while len(line) >= -pos and line[pos:][0] == "\\":
    131          pos -= 1
    132          count += 1
    133      return (count % 2) == 1  # Odd is a line continuation, even is not
    134
    135 -def key_strip(key):
    136      """Cleanup whitespace found around a key
    137
    138      @param key: A properties key
    139      @type key: str
    140      @return: Key without any uneeded whitespace
    141      @rtype: str
    142      """
    143      newkey = key.rstrip()
    144      # If line now end in \ we put back the whitespace that was escaped
    145      if newkey[-1:] == "\\":
    146          newkey += key[len(newkey):len(newkey)+1]
    147      return newkey.lstrip()
    148
    149  default_encoding = {"java": "latin1", "mozilla": "utf-8", "skype": "utf-16"}
    150
    151 -class propunit(base.TranslationUnit):
    152      """an element of a properties file i.e. a name and value, and any comments
    153      associated"""
    154 -    def __init__(self, source="", personality="java"):
    155          """construct a blank propunit"""
    156          self.personality = personality
    157          super(propunit, self).__init__(source)
    158          self.name = ""
    159          self.value = u""
    160          self.translation = u""
    161          self.delimiter = u"="
    162          self.comments = []
    163          self.source = source
    164
    165 -    def setsource(self, source):
    166          self._rich_source = None
    167          source = data.forceunicode(source)
    168          if self.personality == "mozilla" or self.personality == "skype":
    169              self.value = quote.mozillapropertiesencode(source or u"")
    170          else:
    171              self.value = quote.javapropertiesencode(source or u"")
    172
    173 -    def getsource(self):
    174          value = quote.propertiesdecode(self.value)
    175          value = re.sub(u"\\\\ ", u" ", value)
    176          return value
    177
    178      source = property(getsource, setsource)
    179
    180 -    def settarget(self, target):
    181          self._rich_target = None
    182          target = data.forceunicode(target)
    183          if self.personality == "mozilla" or self.personality == "skype":
    184              self.translation = quote.mozillapropertiesencode(target or u"")
    185          else:
    186              self.translation = quote.javapropertiesencode(target or u"")
    187
    188 -    def gettarget(self):
    189          translation = quote.propertiesdecode(self.translation)
    190          translation = re.sub(u"\\\\ ", u" ", translation)
    191          return translation
    192
    193      target = property(gettarget, settarget)
    194
    195 -    def __str__(self):
    196          """convert to a string. double check that unicode is handled somehow here"""
    197          source = self.getoutput()
    198          if isinstance(source, unicode):
    199              return source.encode(default_encoding[self.personality])
    200          return source
    201
    202 -    def getoutput(self):
    203          """convert the element back into formatted lines for a .properties file"""
    204          notes = self.getnotes()
    205          if notes:
    206              notes += u"\n"
    207          if self.isblank():
    208              return notes
    209          else:
    210              if "\\u" in self.value and self.personality == "mozilla":
    211                  self.value = quote.mozillapropertiesencode(self.source)
    212              if "\\u" in self.translation and self.personality == "mozilla":
    213                  self.translation = quote.mozillapropertiesencode(self.target)
    214              value = self.translation or self.value
    215              return u"%s%s%s%s\n" % (notes, self.name, self.delimiter, value)
    216
    217 -    def getlocations(self):
    218          return [self.name]
    219
    220 -    def addnote(self, text, origin=None, position="append"):
    221          if origin in ['programmer', 'developer', 'source code', None]:
    222              text = data.forceunicode(text)
    223              self.comments.append(text)
    224          else:
    225              return super(propunit, self).addnote(text, origin=origin, position=position)
    226
    227 -    def getnotes(self, origin=None):
    228          if origin in ['programmer', 'developer', 'source code', None]:
    229              return u'\n'.join(self.comments)
    230          else:
    231              return super(propunit, self).getnotes(origin)
    232
    233 -    def removenotes(self):
    234          self.comments = []
    235
    236 -    def isblank(self):
    237          """returns whether this is a blank element, containing only comments..."""
    238          return not (self.name or self.value)
    239
    240 -    def istranslatable(self):
    241          return bool(self.name)
    242
    243 -    def getid(self):
    244          return self.name
    245
    246 -class propfile(base.TranslationStore):
    247      """this class represents a .properties file, made up of propunits"""
    248      UnitClass = propunit
    249 -    def __init__(self, inputfile=None, personality="java"):
    250          """construct a propfile, optionally reading in from inputfile"""
    251          super(propfile, self).__init__(unitclass = self.UnitClass)
    252          self.filename = getattr(inputfile, 'name', '')
    253          if inputfile is not None:
    254              propsrc = inputfile.read()
    255              inputfile.close()
    256              self.parse(propsrc, personality)
    257
    258 -    def parse(self, propsrc, personality="java"):
    259          """read the source of a properties file in and include them as units"""
    260          newunit = propunit("", personality)
    261          inmultilinevalue = False
    262          propsrc = unicode(propsrc, default_encoding[personality])
    263          for line in propsrc.split(u"\n"):
    264              # handle multiline value if we're in one
    265              line = quote.rstripeol(line)
    266              if inmultilinevalue:
    267                  newunit.value += line.lstrip()
    268                  # see if there's more
    269                  inmultilinevalue = is_line_continuation(newunit.value)
    270                  # if we're still waiting for more...
    271                  if inmultilinevalue:
    272                      # strip the backslash
    273                      newunit.value = newunit.value[:-1]
    274                  if not inmultilinevalue:
    275                      # we're finished, add it to the list...
    276                      self.addunit(newunit)
    277                      newunit = propunit("", personality)
    278              # otherwise, this could be a comment
    279              elif line.strip()[:1] in (u'#', u'!'):
    280                  # add a comment
    281                  newunit.comments.append(line)
    282              elif not line.strip():
    283                  # this is a blank line...
    284                  if str(newunit).strip():
    285                      self.addunit(newunit)
    286                      newunit = propunit("", personality)
    287              else:
    288                  delimiter_char, delimiter_pos = find_delimiter(line)
    289                  if delimiter_pos == -1:
    290                      continue
    291                  # otherwise, this is a definition
    292                  else:
    293                      newunit.delimiter = delimiter_char
    294                      newunit.name = key_strip(line[:delimiter_pos])
    295                      newunit.value = line[delimiter_pos+1:].lstrip()
    296                      # backslash at end means carry string on to next line
    297                      if is_line_continuation(newunit.value):
    298                          inmultilinevalue = True
    299                          newunit.value = newunit.value[:-1]
    300                      else:
    301                          self.addunit(newunit)
    302                          newunit = propunit("", personality)
    303          # see if there is a leftover one...
    304          if inmultilinevalue or len(newunit.comments) > 0:
    305              self.addunit(newunit)
    306
    307 -    def __str__(self):
    308          """convert the units back to lines"""
    309          lines = []
    310          for unit in self.units:
    311              lines.append(str(unit))
    312          return "".join(lines)
    313