pygettext.py
上传用户:gyjinxi
上传日期:2007-01-04
资源大小:159k
文件大小:11k
源码类别:

WEB邮件程序

开发平台:

Python

  1. #! /usr/bin/env python
  2. # Originally written by Barry Warsaw <bwarsaw@python.org>
  3. """pygettext -- Python equivalent of xgettext(1)
  4. Many systems (Solaris, Linux, Gnu) provide extensive tools that ease the
  5. internationalization of C programs.  Most of these tools are independent of
  6. the programming language and can be used from within Python programs.  Martin
  7. von Loewis' work[1] helps considerably in this regard.
  8. There's one problem though; xgettext is the program that scans source code
  9. looking for message strings, but it groks only C (or C++).  Python introduces
  10. a few wrinkles, such as dual quoting characters, triple quoted strings, and
  11. raw strings.  xgettext understands none of this.
  12. Enter pygettext, which uses Python's standard tokenize module to scan Python
  13. source code, generating .pot files identical to what GNU xgettext[2] generates
  14. for C and C++ code.  From there, the standard GNU tools can be used.
  15. A word about marking Python strings as candidates for translation.  GNU
  16. xgettext recognizes the following keywords: gettext, dgettext, dcgettext, and
  17. gettext_noop.  But those can be a lot of text to include all over your code.
  18. C and C++ have a trick: they use the C preprocessor.  Most internationalized C
  19. source includes a #define for gettext() to _() so that what has to be written
  20. in the source is much less.  Thus these are both translatable strings:
  21.     gettext("Translatable String")
  22.     _("Translatable String")
  23. Python of course has no preprocessor so this doesn't work so well.  Thus,
  24. pygettext searches only for _() by default, but see the -k/--keyword flag
  25. below for how to augment this.
  26.  [1] http://www.python.org/workshops/1997-10/proceedings/loewis.html
  27.  [2] http://www.gnu.org/software/gettext/gettext.html
  28. NOTE: pygettext attempts to be option and feature compatible with GNU xgettext
  29. where ever possible.
  30. Usage: pygettext [options] filename ...
  31. Options:
  32.     -a
  33.     --extract-all
  34.         Extract all strings
  35.     -d default-domain
  36.     --default-domain=default-domain
  37.         Rename the default output file from messages.pot to default-domain.pot 
  38.     -k [word]
  39.     --keyword[=word]
  40.         Additional keywords to look for.  Without `word' means not to use the
  41.         default keywords.  The default keywords, which are always looked for
  42.         if not explicitly disabled: _
  43.         The default keyword list is different than GNU xgettext. You can have
  44.         multiple -k flags on the command line.
  45.     --no-location
  46.         Do not write filename/lineno location comments
  47.     -n [style]
  48.     --add-location[=style]
  49.         Write filename/lineno location comments indicating where each
  50.         extracted string is found in the source.  These lines appear before
  51.         each msgid.  Two styles are supported:
  52.         Solaris  # File: filename, line: line-number
  53.         Gnu      #: filename:line
  54.         If style is omitted, Gnu is used.  The style name is case
  55.         insensitive.  By default, locations are included.
  56.     -v
  57.     --verbose
  58.         Print the names of the files being processed.
  59.     --help
  60.     -h
  61.         print this help message and exit
  62. """
  63. import os
  64. import sys
  65. import string
  66. import time
  67. import getopt
  68. import tokenize
  69. __version__ = '0.2-1'
  70. # for selftesting
  71. def _(s): return s
  72. # The normal pot-file header. msgmerge and EMACS' po-mode work better if
  73. # it's there.
  74. pot_header = _('''
  75. # SOME DESCRIPTIVE TITLE.
  76. # Copyright (C) YEAR ORGANIZATION
  77. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
  78. #
  79. msgid ""
  80. msgstr ""
  81. "Project-Id-Version: PACKAGE VERSION\n"
  82. "PO-Revision-Date: %(time)s\n"
  83. "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
  84. "Language-Team: LANGUAGE <LL@li.org>\n"
  85. "MIME-Version: 1.0\n"
  86. "Content-Type: text/plain; charset=CHARSET\n"
  87. "Content-Transfer-Encoding: ENCODING\n"
  88. "Generated-By: pygettext.py %(version)s\n"
  89. ''')
  90. def usage(code, msg=''):
  91.     print __doc__ % globals()
  92.     if msg:
  93.         print msg
  94.     sys.exit(code)
  95. escapes = []
  96. for i in range(256):
  97.     if i < 32 or i > 127:
  98.         escapes.append("\%03o" % i)
  99.     else:
  100.         escapes.append(chr(i))
  101. escapes[ord('\')] = '\\'
  102. escapes[ord('t')] = '\t'
  103. escapes[ord('r')] = '\r'
  104. escapes[ord('n')] = '\n'
  105. def escape(s):
  106.     s = list(s)
  107.     for i in range(len(s)):
  108.         s[i] = escapes[ord(s[i])]
  109.     return string.join(s, '')
  110. def safe_eval(s):
  111.     # unwrap quotes, safely
  112.     return eval(s, {'__builtins__':{}}, {})
  113. def normalize(s):
  114.     # This converts the various Python string types into a format that is
  115.     # appropriate for .po files, namely much closer to C style.
  116.     lines = string.split(s, 'n')
  117.     if len(lines) == 1:
  118.         s = '"' + escape(s) + '"'
  119.     else:
  120.         if not lines[-1]:
  121.             del lines[-1]
  122.             lines[-1] = lines[-1] + 'n'
  123.         for i in range(len(lines)):
  124.             lines[i] = escape(lines[i])
  125.         s = '""n"' + string.join(lines, '\n"n"') + '"'
  126.     return s
  127. try: from cStringIO import StringIO
  128. except: from StringIO import StringIO
  129. from string import find, split
  130. from sgmllib import SGMLParser
  131. class I18NParser(SGMLParser):
  132. gettext_tags = ["dtml-gettext", "dtml-gt"]
  133. def __init__(self):
  134. SGMLParser.__init__(self)
  135. self.data = ""
  136. self.messages = []
  137. self.startpos = 0
  138. def unknown_starttag(self, tag, attrs):
  139.     for k, v in attrs:
  140. for t in self.gettext_tags:
  141.     if find(v, t):
  142. p = I18NParser()
  143. p.feed(v)
  144. if p.messages: self.messages.extend(p.messages)
  145. break
  146.     if tag in self.gettext_tags:
  147. self.data = ""
  148. def unknown_endtag(self, tag):
  149. if tag in self.gettext_tags:
  150. f = find(self.rawdata, self.data, self.startpos)
  151. row = len(split(self.rawdata[:f], "n"))
  152. self.messages.append((self.data, row))
  153. self.startpos = f + 1
  154. self.data = ""
  155. def handle_entityref(self, name): self.handle_data("&%s;" % name)
  156. def handle_charref(self, name): self.handle_data("&#%s;" % name)
  157. def handle_comment(self, data): self.handle_data("<!--%s-->" % data)
  158. def handle_data(self, data): self.data = self.data + data
  159. def htmlfilter(fp):
  160.     buf, row = "", 1
  161.     p = I18NParser()
  162.     p.feed(fp.read())
  163.     for msg, lineno in p.messages:
  164. while row < lineno:
  165.     buf = buf + "n"
  166.     row = row + 1
  167. buf = buf + '_("%s"); ' % msg
  168.     return StringIO(buf)
  169. class TokenEater:
  170.     def __init__(self, options):
  171.         self.__options = options
  172.         self.__messages = {}
  173.         self.__state = self.__waiting
  174.         self.__data = []
  175.         self.__lineno = -1
  176.     def __call__(self, ttype, tstring, stup, etup, line):
  177.         # dispatch
  178.         self.__state(ttype, tstring, stup[0])
  179.     def __waiting(self, ttype, tstring, lineno):
  180.         if ttype == tokenize.NAME and tstring in self.__options.keywords:
  181.             self.__state = self.__keywordseen
  182.     def __keywordseen(self, ttype, tstring, lineno):
  183.         if ttype == tokenize.OP and tstring == '(':
  184.             self.__data = []
  185.             self.__lineno = lineno
  186.             self.__state = self.__openseen
  187.         else:
  188.             self.__state = self.__waiting
  189.     def __openseen(self, ttype, tstring, lineno):
  190.         if ttype == tokenize.OP and tstring == ')':
  191.             # We've seen the last of the translatable strings.  Record the
  192.             # line number of the first line of the strings and update the list 
  193.             # of messages seen.  Reset state for the next batch.  If there
  194.             # were no strings inside _(), then just ignore this entry.
  195.             if self.__data:
  196.                 msg = string.join(self.__data, '')
  197.                 entry = (self.__curfile, self.__lineno)
  198.                 linenos = self.__messages.get(msg)
  199.                 if linenos is None:
  200.                     self.__messages[msg] = [entry]
  201.                 else:
  202.                     linenos.append(entry)
  203.             self.__state = self.__waiting
  204.         elif ttype == tokenize.STRING:
  205.             self.__data.append(safe_eval(tstring))
  206.         # TBD: should we warn if we seen anything else?
  207.     def set_filename(self, filename):
  208.         self.__curfile = filename
  209.     def write(self, fp):
  210.         options = self.__options
  211.         timestamp = time.ctime(time.time())
  212.         # common header
  213.         try:
  214.             sys.stdout = fp
  215.             # The time stamp in the header doesn't have the same format
  216.             # as that generated by xgettext...
  217.             print pot_header % {'time': timestamp, 'version':__version__}
  218.             for k, v in self.__messages.items():
  219.                 for filename, lineno in v:
  220.                     # location comments are different b/w Solaris and GNU
  221.                     d = {'filename': filename,
  222.                          'lineno': lineno}
  223.                     if options.location == options.SOLARIS:
  224.                         print _('# File: %(filename)s, line: %(lineno)d') % d
  225.                     elif options.location == options.GNU:
  226.                         print _('#: %(filename)s:%(lineno)d') % d
  227.                 # TBD: sorting, normalizing
  228.                 print 'msgid', normalize(k)
  229.                 print 'msgstr ""'
  230.                 print
  231.         finally:
  232.             sys.stdout = sys.__stdout__
  233. def main():
  234.     default_keywords = ['_']
  235.     try:
  236.         opts, args = getopt.getopt(
  237.             sys.argv[1:],
  238.             'k:d:n:hv',
  239.             ['keyword', 'default-domain', 'help',
  240.              'add-location=', 'no-location', 'verbose'])
  241.     except getopt.error, msg:
  242.         usage(1, msg)
  243.     # for holding option values
  244.     class Options:
  245.         # constants
  246.         GNU = 1
  247.         SOLARIS = 2
  248.         # defaults
  249.         keywords = []
  250.         outfile = 'messages.pot'
  251.         location = GNU
  252.         verbose = 0
  253.     options = Options()
  254.     locations = {'gnu' : options.GNU,
  255.                  'solaris' : options.SOLARIS,
  256.                  }
  257.     # parse options
  258.     for opt, arg in opts:
  259.         if opt in ('-h', '--help'):
  260.             usage(0)
  261.         elif opt in ('-k', '--keyword'):
  262.             if arg is None:
  263.                 default_keywords = []
  264.             options.keywords.append(arg)
  265.         elif opt in ('-d', '--default-domain'):
  266.             options.outfile = arg + '.pot'
  267.         elif opt in ('-n', '--add-location'):
  268.             if arg is None:
  269.                 arg = 'gnu'
  270.             try:
  271.                 options.location = locations[string.lower(arg)]
  272.             except KeyError:
  273.                 d = {'arg':arg}
  274.                 usage(1, _('Invalid value for --add-location: %(arg)s') % d)
  275.         elif opt in ('--no-location',):
  276.             options.location = 0
  277.         elif opt in ('-v', '--verbose'):
  278.             options.verbose = 1
  279.     # calculate all keywords
  280.     options.keywords.extend(default_keywords)
  281.     # slurp through all the files
  282.     eater = TokenEater(options)
  283.     for filename in args:
  284.         if options.verbose:
  285.             print _('Working on %(filename)s') % {'filename':filename}
  286. ext = os.path.splitext(filename)[1]
  287. if ext in [".htm", ".html"]:
  288.     fp = htmlfilter(open(filename))
  289. else: fp = open(filename)
  290.         eater.set_filename(filename)
  291.         tokenize.tokenize(fp.readline, eater)
  292.         fp.close()
  293.     fp = open(options.outfile, 'w')
  294.     eater.write(fp)
  295.     fp.close()
  296. if __name__ == '__main__':
  297.     main()