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_delimeter(line):
     63      """Find the type and position of the delimeter in a property line.
     64
     65      Property files can be delimeted by "=", ":" or whitespace (space for now).
     66      We find the position of each delimeter, then find the one that appears
     67      first.
     68
     69      @param line: A properties line
     70      @type line: str
     71      @return: Delimeter character and offset within L{line}
     72      @rtype: Tuple (Delimeter char, Offset Integer)
     73      """
     74      delimeters = {"=": -1, ":": -1, " ": -1}
     75      # Find the position of each delimeter type
     76      for delimeter, pos in delimeters.iteritems():
     77          prewhitespace = len(line) - len(line.lstrip())
     78          pos = line.find(delimeter, prewhitespace)
     79          while pos != -1:
     80              if delimeters[delimeter] == -1 and line[pos-1] != "\\":
     81                  delimeters[delimeter] = pos
     82                  break
     83              pos = line.find(delimeter, pos+1)
     84      # Find the first "=" or ":" delimeter
     85      mindelimeter = None
     86      minpos = -1
     87      for delimeter, pos in delimeters.iteritems():
     88          if pos == -1 or delimeter == " ":
     89              continue
     90          if minpos == -1 or pos < minpos:
     91              minpos = pos
     92              mindelimeter = delimeter
     93      if mindelimeter is None and delimeters[" "] != -1:
     94          # Use space delimeter if we found nothing else
     95          return (" ", delimeters[" "])
     96      if mindelimeter is not None and delimeters[" "] < delimeters[mindelimeter]:
     97          # If space delimeter occurs earlier then ":" or "=" then it is the
     98          # delimeter only if there are non-whitespace characters between it and
     99          # the other detected delimeter.
    100          if len(line[delimeters[" "]:delimeters[mindelimeter]].strip()) > 0:
    101              return (" ", delimeters[" "])
    102      return (mindelimeter, minpos)
    103
    104 -def is_line_continuation(line):
    105      """Determine whether L{line} has a line continuation marker.
    106
    107      .properties files can be terminated with a backslash (\\) indicating
    108      that the 'value' continues on the next line.  Continuation is only
    109      valid if there are an odd number of backslashses (an even number
    110      would result in a set of N/2 slashes not an escape)
    111
    112      @param line: A properties line
    113      @type line: str
    114      @return: Does L{line} end with a line continuation
    115      @rtype: Boolean
    116      """
    117      pos = -1
    118      count = 0
    119      if len(line) == 0:
    120          return False
    121      # Count the slashes from the end of the line. Ensure we don't
    122      # go into infinite loop.
    123      while len(line) >= -pos and line[pos:][0] == "\\":
    124          pos -= 1
    125          count += 1
    126      return (count % 2) == 1  # Odd is a line continuation, even is not
    127
    128 -def key_strip(key):
    129      """Cleanup whitespace found around a key
    130
    131      @param key: A properties key
    132      @type key: str
    133      @return: Key without any uneeded whitespace
    134      @rtype: str
    135      """
    136      newkey = key.rstrip()
    137      # If line now end in \ we put back the whitespace that was escaped
    138      if newkey[-1:] == "\\":
    139          newkey += key[len(newkey):len(newkey)+1]
    140      return newkey.lstrip()
    141
    142  default_encoding = {"java": "latin1", "mozilla": "utf-8", "skype": "utf-16"}
    143
    144 -class propunit(base.TranslationUnit):
    145      """an element of a properties file i.e. a name and value, and any comments
    146      associated"""
    147 -    def __init__(self, source="", personality="java"):
    148          """construct a blank propunit"""
    149          self.personality = personality
    150          super(propunit, self).__init__(source)
    151          self.name = ""
    152          self.value = u""
    153          self.translation = u""
    154          self.delimeter = u"="
    155          self.comments = []
    156          self.source = source
    157
    158 -    def setsource(self, source):
    159          source = data.forceunicode(source)
    160          if self.personality == "mozilla" or self.personality == "skype":
    161              self.value = quote.mozillapropertiesencode(source or u"")
    162          else:
    163              self.value = quote.javapropertiesencode(source or u"")
    164
    165 -    def getsource(self):
    166          value = quote.propertiesdecode(self.value)
    167          value = re.sub(u"\\\\ ", u" ", value)
    168          return value
    169
    170      source = property(getsource, setsource)
    171
    172 -    def settarget(self, target):
    173          target = data.forceunicode(target)
    174          if self.personality == "mozilla" or self.personality == "skype":
    175              self.translation = quote.mozillapropertiesencode(target or u"")
    176          else:
    177              self.translation = quote.javapropertiesencode(target or u"")
    178
    179 -    def gettarget(self):
    180          translation = quote.propertiesdecode(self.translation)
    181          translation = re.sub(u"\\\\ ", u" ", translation)
    182          return translation
    183
    184      target = property(gettarget, settarget)
    185
    186 -    def __str__(self):
    187          """convert to a string. double check that unicode is handled somehow here"""
    188          source = self.getoutput()
    189          if isinstance(source, unicode):
    190              return source.encode(default_encoding[self.personality])
    191          return source
    192
    193 -    def getoutput(self):
    194          """convert the element back into formatted lines for a .properties file"""
    195          notes = self.getnotes()
    196          if notes:
    197              notes += u"\n"
    198          if self.isblank():
    199              return notes
    200          else:
    201              if "\\u" in self.value and self.personality == "mozilla":
    202                  self.value = quote.mozillapropertiesencode(self.source)
    203              if "\\u" in self.translation and self.personality == "mozilla":
    204                  self.translation = quote.mozillapropertiesencode(self.target)
    205              value = self.translation or self.value
    206              return u"%s%s%s%s\n" % (notes, self.name, self.delimeter, value)
    207
    208 -    def getlocations(self):
    209          return [self.name]
    210
    211 -    def addnote(self, text, origin=None, position="append"):
    212          if origin in ['programmer', 'developer', 'source code', None]:
    213              text = data.forceunicode(text)
    214              self.comments.append(text)
    215          else:
    216              return super(propunit, self).addnote(text, origin=origin, position=position)
    217
    218 -    def getnotes(self, origin=None):
    219          if origin in ['programmer', 'developer', 'source code', None]:
    220              return u'\n'.join(self.comments)
    221          else:
    222              return super(propunit, self).getnotes(origin)
    223
    224 -    def removenotes(self):
    225          self.comments = []
    226
    227 -    def isblank(self):
    228          """returns whether this is a blank element, containing only comments..."""
    229          return not (self.name or self.value)
    230
    231 -    def istranslatable(self):
    232          return bool(self.name)
    233
    234 -    def getid(self):
    235          return self.name
    236
    237 -class propfile(base.TranslationStore):
    238      """this class represents a .properties file, made up of propunits"""
    239      UnitClass = propunit
    240 -    def __init__(self, inputfile=None, personality="java"):
    241          """construct a propfile, optionally reading in from inputfile"""
    242          super(propfile, self).__init__(unitclass = self.UnitClass)
    243          self.filename = getattr(inputfile, 'name', '')
    244          if inputfile is not None:
    245              propsrc = inputfile.read()
    246              inputfile.close()
    247              self.parse(propsrc, personality)
    248
    249 -    def parse(self, propsrc, personality="java"):
    250          """read the source of a properties file in and include them as units"""
    251          newunit = propunit("", personality)
    252          inmultilinevalue = False
    253          propsrc = unicode(propsrc, default_encoding[personality])
    254          for line in propsrc.split(u"\n"):
    255              # handle multiline value if we're in one
    256              line = quote.rstripeol(line)
    257              if inmultilinevalue:
    258                  newunit.value += line.lstrip()
    259                  # see if there's more
    260                  inmultilinevalue = is_line_continuation(newunit.value)
    261                  # if we're still waiting for more...
    262                  if inmultilinevalue:
    263                      # strip the backslash
    264                      newunit.value = newunit.value[:-1]
    265                  if not inmultilinevalue:
    266                      # we're finished, add it to the list...
    267                      self.addunit(newunit)
    268                      newunit = propunit("", personality)
    269              # otherwise, this could be a comment
    270              elif line.strip()[:1] in (u'#', u'!'):
    271                  # add a comment
    272                  newunit.comments.append(line)
    273              elif not line.strip():
    274                  # this is a blank line...
    275                  if str(newunit).strip():
    276                      self.addunit(newunit)
    277                      newunit = propunit("", personality)
    278              else:
    279                  delimeter_char, delimeter_pos = find_delimeter(line)
    280                  if delimeter_pos == -1:
    281                      continue
    282                  # otherwise, this is a definition
    283                  else:
    284                      newunit.delimeter = delimeter_char
    285                      newunit.name = key_strip(line[:delimeter_pos])
    286                      newunit.value = line[delimeter_pos+1:].lstrip()
    287                      # backslash at end means carry string on to next line
    288                      if is_line_continuation(newunit.value):
    289                          inmultilinevalue = True
    290                          newunit.value = newunit.value[:-1]
    291                      else:
    292                          self.addunit(newunit)
    293                          newunit = propunit("", personality)
    294          # see if there is a leftover one...
    295          if inmultilinevalue or len(newunit.comments) > 0:
    296              self.addunit(newunit)
    297
    298 -    def __str__(self):
    299          """convert the units back to lines"""
    300          lines = []
    301          for unit in self.units:
    302              lines.append(str(unit))
    303          return "".join(lines)
    304