~ubuntu-branches/debian/sid/adonthell-data/sid

« back to all changes in this revision

Viewing changes to po/pygettext.py

  • Committer: Bazaar Package Importer
  • Author(s): Ana Beatriz Guerrero Lopez, Luis Rodrigo Gallardo Cruz, Ana Beatriz Guerrero Lopez
  • Date: 2006-10-28 22:05:25 UTC
  • mfrom: (1.1.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20061028220525-m73ne9y9nputzqqv
Tags: 0.3.4.cvs.20050903-2.2
* Non-maintainer upload.

[ Luis Rodrigo Gallardo Cruz ]
* Stop shipping *.pyc files (Closes: #390573).

[ Ana Beatriz Guerrero Lopez ]
* Moved debhelper and quilt to Build-Depends. 
* Little changes to Rodrigo's patch, and added patch that fixes 
  player_text.py
* Bumped standards-version to 3.7.2, no changes required.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#! /usr/bin/env python
 
2
# Originally written by Barry Warsaw <barry@zope.com>
 
3
#
 
4
# Minimally patched to make it even more xgettext compatible 
 
5
# by Peter Funk <pf@artcom-gmbh.de>
 
6
 
 
7
"""pygettext -- Python equivalent of xgettext(1)
 
8
 
 
9
Many systems (Solaris, Linux, Gnu) provide extensive tools that ease the
 
10
internationalization of C programs.  Most of these tools are independent of
 
11
the programming language and can be used from within Python programs.  Martin
 
12
von Loewis' work[1] helps considerably in this regard.
 
13
 
 
14
There's one problem though; xgettext is the program that scans source code
 
15
looking for message strings, but it groks only C (or C++).  Python introduces
 
16
a few wrinkles, such as dual quoting characters, triple quoted strings, and
 
17
raw strings.  xgettext understands none of this.
 
18
 
 
19
Enter pygettext, which uses Python's standard tokenize module to scan Python
 
20
source code, generating .pot files identical to what GNU xgettext[2] generates
 
21
for C and C++ code.  From there, the standard GNU tools can be used.
 
22
 
 
23
A word about marking Python strings as candidates for translation.  GNU
 
24
xgettext recognizes the following keywords: gettext, dgettext, dcgettext, and
 
25
gettext_noop.  But those can be a lot of text to include all over your code.
 
26
C and C++ have a trick: they use the C preprocessor.  Most internationalized C
 
27
source includes a #define for gettext() to _() so that what has to be written
 
28
in the source is much less.  Thus these are both translatable strings:
 
29
 
 
30
    gettext("Translatable String")
 
31
    _("Translatable String")
 
32
 
 
33
Python of course has no preprocessor so this doesn't work so well.  Thus,
 
34
pygettext searches only for _() by default, but see the -k/--keyword flag
 
35
below for how to augment this.
 
36
 
 
37
 [1] http://www.python.org/workshops/1997-10/proceedings/loewis.html
 
38
 [2] http://www.gnu.org/software/gettext/gettext.html
 
39
 
 
40
NOTE: pygettext attempts to be option and feature compatible with GNU xgettext
 
41
where ever possible.  However some options are still missing or are not fully
 
42
implemented.  Also, xgettext's use of command line switches with option
 
43
arguments is broken, and in these cases, pygettext just defines additional
 
44
switches.
 
45
 
 
46
Usage: pygettext [options] inputfile ...
 
47
 
 
48
Options:
 
49
 
 
50
    -a
 
51
    --extract-all
 
52
        Extract all strings.
 
53
 
 
54
    -d name
 
55
    --default-domain=name
 
56
        Rename the default output file from messages.pot to name.pot.
 
57
 
 
58
    -E
 
59
    --escape
 
60
        Replace non-ASCII characters with octal escape sequences.
 
61
 
 
62
    -D
 
63
    --docstrings
 
64
        Extract module, class, method, and function docstrings.  These do not
 
65
        need to be wrapped in _() markers, and in fact cannot be for Python to
 
66
        consider them docstrings. (See also the -X option).
 
67
 
 
68
    -h
 
69
    --help
 
70
        Print this help message and exit.
 
71
 
 
72
    -k word
 
73
    --keyword=word
 
74
        Keywords to look for in addition to the default set, which are:
 
75
        %(DEFAULTKEYWORDS)s
 
76
 
 
77
        You can have multiple -k flags on the command line.
 
78
 
 
79
    -K
 
80
    --no-default-keywords
 
81
        Disable the default set of keywords (see above).  Any keywords
 
82
        explicitly added with the -k/--keyword option are still recognized.
 
83
 
 
84
    --no-location
 
85
        Do not write filename/lineno location comments.
 
86
 
 
87
    -n
 
88
    --add-location
 
89
        Write filename/lineno location comments indicating where each
 
90
        extracted string is found in the source.  These lines appear before
 
91
        each msgid.  The style of comments is controlled by the -S/--style
 
92
        option.  This is the default.
 
93
 
 
94
    -o filename
 
95
    --output=filename
 
96
        Rename the default output file from messages.pot to filename.  If
 
97
        filename is `-' then the output is sent to standard out.
 
98
 
 
99
    -p dir
 
100
    --output-dir=dir
 
101
        Output files will be placed in directory dir.
 
102
 
 
103
    -S stylename
 
104
    --style stylename
 
105
        Specify which style to use for location comments.  Two styles are
 
106
        supported:
 
107
 
 
108
        Solaris  # File: filename, line: line-number
 
109
        GNU      #: filename:line
 
110
 
 
111
        The style name is case insensitive.  GNU style is the default.
 
112
 
 
113
    -v
 
114
    --verbose
 
115
        Print the names of the files being processed.
 
116
 
 
117
    -V
 
118
    --version
 
119
        Print the version of pygettext and exit.
 
120
 
 
121
    -w columns
 
122
    --width=columns
 
123
        Set width of output to columns.
 
124
 
 
125
    -x filename
 
126
    --exclude-file=filename
 
127
        Specify a file that contains a list of strings that are not be
 
128
        extracted from the input files.  Each string to be excluded must
 
129
        appear on a line by itself in the file.
 
130
 
 
131
    -X filename
 
132
    --no-docstrings=filename
 
133
        Specify a file that contains a list of files (one per line) that
 
134
        should not have their docstrings extracted.  This is only useful in
 
135
        conjunction with the -D option above.
 
136
 
 
137
If `inputfile' is -, standard input is read.
 
138
"""
 
139
 
 
140
import os
 
141
import sys
 
142
import time
 
143
import getopt
 
144
import tokenize
 
145
import operator
 
146
 
 
147
# for selftesting
 
148
try:
 
149
    import fintl
 
150
    _ = fintl.gettext
 
151
except ImportError:
 
152
    def _(s): return s
 
153
 
 
154
__version__ = '1.4'
 
155
 
 
156
default_keywords = ['_']
 
157
DEFAULTKEYWORDS = ', '.join(default_keywords)
 
158
 
 
159
EMPTYSTRING = ''
 
160
 
 
161
 
 
162
 
 
163
# The normal pot-file header. msgmerge and Emacs's po-mode work better if it's
 
164
# there.
 
165
pot_header = _('''\
 
166
# SOME DESCRIPTIVE TITLE.
 
167
# Copyright (C) YEAR ORGANIZATION
 
168
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
 
169
#
 
170
msgid ""
 
171
msgstr ""
 
172
"Project-Id-Version: PACKAGE VERSION\\n"
 
173
"POT-Creation-Date: %(time)s\\n"
 
174
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"
 
175
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"
 
176
"Language-Team: LANGUAGE <LL@li.org>\\n"
 
177
"MIME-Version: 1.0\\n"
 
178
"Content-Type: text/plain; charset=CHARSET\\n"
 
179
"Content-Transfer-Encoding: ENCODING\\n"
 
180
"Generated-By: pygettext.py %(version)s\\n"
 
181
 
 
182
''')
 
183
 
 
184
 
 
185
def usage(code, msg=''):
 
186
    print >> sys.stderr, _(__doc__) % globals()
 
187
    if msg:
 
188
        print >> sys.stderr, msg
 
189
    sys.exit(code)
 
190
 
 
191
 
 
192
 
 
193
escapes = []
 
194
 
 
195
def make_escapes(pass_iso8859):
 
196
    global escapes
 
197
    if pass_iso8859:
 
198
        # Allow iso-8859 characters to pass through so that e.g. 'msgid
 
199
        # "H�he"' would result not result in 'msgid "H\366he"'.  Otherwise we
 
200
        # escape any character outside the 32..126 range.
 
201
        mod = 128
 
202
    else:
 
203
        mod = 256
 
204
    for i in range(256):
 
205
        if 32 <= (i % mod) <= 126:
 
206
            escapes.append(chr(i))
 
207
        else:
 
208
            escapes.append("\\%03o" % i)
 
209
    escapes[ord('\\')] = '\\\\'
 
210
    escapes[ord('\t')] = '\\t'
 
211
    escapes[ord('\r')] = '\\r'
 
212
    escapes[ord('\n')] = '\\n'
 
213
    escapes[ord('\"')] = '\\"'
 
214
 
 
215
 
 
216
def escape(s):
 
217
    global escapes
 
218
    s = list(s)
 
219
    for i in range(len(s)):
 
220
        s[i] = escapes[ord(s[i])]
 
221
    return EMPTYSTRING.join(s)
 
222
 
 
223
 
 
224
def safe_eval(s):
 
225
    # unwrap quotes, safely
 
226
    return eval(s, {'__builtins__':{}}, {})
 
227
 
 
228
 
 
229
def normalize(s):
 
230
    # This converts the various Python string types into a format that is
 
231
    # appropriate for .po files, namely much closer to C style.
 
232
    lines = s.split('\n')
 
233
    if len(lines) == 1:
 
234
        s = '"' + escape(s) + '"'
 
235
    else:
 
236
        if not lines[-1]:
 
237
            del lines[-1]
 
238
            lines[-1] = lines[-1] + '\n'
 
239
        for i in range(len(lines)):
 
240
            lines[i] = escape(lines[i])
 
241
        lineterm = '\\n"\n"'
 
242
        s = '""\n"' + lineterm.join(lines) + '"'
 
243
    return s
 
244
 
 
245
 
 
246
 
 
247
class TokenEater:
 
248
    def __init__(self, options):
 
249
        self.__options = options
 
250
        self.__messages = {}
 
251
        self.__state = self.__waiting
 
252
        self.__data = []
 
253
        self.__lineno = -1
 
254
        self.__freshmodule = 1
 
255
        self.__curfile = None
 
256
 
 
257
    def __call__(self, ttype, tstring, stup, etup, line):
 
258
        # dispatch
 
259
##        import token
 
260
##        print >> sys.stderr, 'ttype:', token.tok_name[ttype], \
 
261
##              'tstring:', tstring
 
262
        self.__state(ttype, tstring, stup[0])
 
263
 
 
264
    def __waiting(self, ttype, tstring, lineno):
 
265
        opts = self.__options
 
266
        # Do docstring extractions, if enabled
 
267
        if opts.docstrings and not opts.nodocstrings.get(self.__curfile):
 
268
            # module docstring?
 
269
            if self.__freshmodule:
 
270
                if ttype == tokenize.STRING:
 
271
                    self.__addentry(safe_eval(tstring), lineno, isdocstring=1)
 
272
                    self.__freshmodule = 0
 
273
                elif ttype not in (tokenize.COMMENT, tokenize.NL):
 
274
                    self.__freshmodule = 0
 
275
                return
 
276
            # class docstring?
 
277
            if ttype == tokenize.NAME and tstring in ('class', 'def'):
 
278
                self.__state = self.__suiteseen
 
279
                return
 
280
        if ttype == tokenize.NAME and tstring in opts.keywords:
 
281
            self.__state = self.__keywordseen
 
282
 
 
283
    def __suiteseen(self, ttype, tstring, lineno):
 
284
        # ignore anything until we see the colon
 
285
        if ttype == tokenize.OP and tstring == ':':
 
286
            self.__state = self.__suitedocstring
 
287
 
 
288
    def __suitedocstring(self, ttype, tstring, lineno):
 
289
        # ignore any intervening noise
 
290
        if ttype == tokenize.STRING:
 
291
            self.__addentry(safe_eval(tstring), lineno, isdocstring=1)
 
292
            self.__state = self.__waiting
 
293
        elif ttype not in (tokenize.NEWLINE, tokenize.INDENT,
 
294
                           tokenize.COMMENT):
 
295
            # there was no class docstring
 
296
            self.__state = self.__waiting
 
297
 
 
298
    def __keywordseen(self, ttype, tstring, lineno):
 
299
        if ttype == tokenize.OP and tstring == '(':
 
300
            self.__data = []
 
301
            self.__lineno = lineno
 
302
            self.__state = self.__openseen
 
303
        else:
 
304
            self.__state = self.__waiting
 
305
 
 
306
    def __openseen(self, ttype, tstring, lineno):
 
307
        if ttype == tokenize.OP and tstring == ')':
 
308
            # We've seen the last of the translatable strings.  Record the
 
309
            # line number of the first line of the strings and update the list 
 
310
            # of messages seen.  Reset state for the next batch.  If there
 
311
            # were no strings inside _(), then just ignore this entry.
 
312
            if self.__data:
 
313
                self.__addentry(EMPTYSTRING.join(self.__data))
 
314
            self.__state = self.__waiting
 
315
        elif ttype == tokenize.STRING:
 
316
            self.__data.append(safe_eval(tstring))
 
317
        # TBD: should we warn if we seen anything else?
 
318
 
 
319
    def __addentry(self, msg, lineno=None, isdocstring=0):
 
320
        if lineno is None:
 
321
            lineno = self.__lineno
 
322
        if not msg in self.__options.toexclude:
 
323
            entry = (self.__curfile, lineno)
 
324
            self.__messages.setdefault(msg, {})[entry] = isdocstring
 
325
 
 
326
    def set_filename(self, filename):
 
327
        self.__curfile = filename
 
328
        self.__freshmodule = 1
 
329
 
 
330
    def write(self, fp):
 
331
        options = self.__options
 
332
        timestamp = time.ctime(time.time())
 
333
        # The time stamp in the header doesn't have the same format as that
 
334
        # generated by xgettext...
 
335
        print >> fp, pot_header % {'time': timestamp, 'version': __version__}
 
336
        # Sort the entries.  First sort each particular entry's keys, then
 
337
        # sort all the entries by their first item.
 
338
        reverse = {}
 
339
        for k, v in self.__messages.items():
 
340
            keys = v.keys()
 
341
            keys.sort()
 
342
            reverse.setdefault(tuple(keys), []).append((k, v))
 
343
        rkeys = reverse.keys()
 
344
        rkeys.sort()
 
345
        for rkey in rkeys:
 
346
            rentries = reverse[rkey]
 
347
            rentries.sort()
 
348
            for k, v in rentries:
 
349
                isdocstring = 0
 
350
                # If the entry was gleaned out of a docstring, then add a
 
351
                # comment stating so.  This is to aid translators who may wish
 
352
                # to skip translating some unimportant docstrings.
 
353
                if reduce(operator.__add__, v.values()):
 
354
                    isdocstring = 1
 
355
                # k is the message string, v is a dictionary-set of (filename,
 
356
                # lineno) tuples.  We want to sort the entries in v first by
 
357
                # file name and then by line number.
 
358
                v = v.keys()
 
359
                v.sort()
 
360
                if not options.writelocations:
 
361
                    pass
 
362
                # location comments are different b/w Solaris and GNU:
 
363
                elif options.locationstyle == options.SOLARIS:
 
364
                    for filename, lineno in v:
 
365
                        d = {'filename': filename, 'lineno': lineno}
 
366
                        print >>fp, _(
 
367
                            '# File: %(filename)s, line: %(lineno)d') % d
 
368
                elif options.locationstyle == options.GNU:
 
369
                    # fit as many locations on one line, as long as the
 
370
                    # resulting line length doesn't exceeds 'options.width'
 
371
                    locline = '#:'
 
372
                    for filename, lineno in v:
 
373
                        d = {'filename': filename, 'lineno': lineno}
 
374
                        s = _(' %(filename)s:%(lineno)d') % d
 
375
                        if len(locline) + len(s) <= options.width:
 
376
                            locline = locline + s
 
377
                        else:
 
378
                            print >> fp, locline
 
379
                            locline = "#:" + s
 
380
                    if len(locline) > 2:
 
381
                        print >> fp, locline
 
382
                if isdocstring:
 
383
                    print >> fp, '#, docstring'
 
384
                print >> fp, 'msgid', normalize(k)
 
385
                print >> fp, 'msgstr ""\n'
 
386
 
 
387
 
 
388
 
 
389
def main():
 
390
    global default_keywords
 
391
    try:
 
392
        opts, args = getopt.getopt(
 
393
            sys.argv[1:],
 
394
            'ad:DEhk:Kno:p:S:Vvw:x:X:',
 
395
            ['extract-all', 'default-domain=', 'escape', 'help',
 
396
             'keyword=', 'no-default-keywords',
 
397
             'add-location', 'no-location', 'output=', 'output-dir=',
 
398
             'style=', 'verbose', 'version', 'width=', 'exclude-file=',
 
399
             'docstrings', 'no-docstrings',
 
400
             ])
 
401
    except getopt.error, msg:
 
402
        usage(1, msg)
 
403
 
 
404
    # for holding option values
 
405
    class Options:
 
406
        # constants
 
407
        GNU = 1
 
408
        SOLARIS = 2
 
409
        # defaults
 
410
        extractall = 0 # FIXME: currently this option has no effect at all.
 
411
        escape = 0
 
412
        keywords = []
 
413
        outpath = ''
 
414
        outfile = 'messages.pot'
 
415
        writelocations = 1
 
416
        locationstyle = GNU
 
417
        verbose = 0
 
418
        width = 78
 
419
        excludefilename = ''
 
420
        docstrings = 0
 
421
        nodocstrings = {}
 
422
 
 
423
    options = Options()
 
424
    locations = {'gnu' : options.GNU,
 
425
                 'solaris' : options.SOLARIS,
 
426
                 }
 
427
 
 
428
    # parse options
 
429
    for opt, arg in opts:
 
430
        if opt in ('-h', '--help'):
 
431
            usage(0)
 
432
        elif opt in ('-a', '--extract-all'):
 
433
            options.extractall = 1
 
434
        elif opt in ('-d', '--default-domain'):
 
435
            options.outfile = arg + '.pot'
 
436
        elif opt in ('-E', '--escape'):
 
437
            options.escape = 1
 
438
        elif opt in ('-D', '--docstrings'):
 
439
            options.docstrings = 1
 
440
        elif opt in ('-k', '--keyword'):
 
441
            options.keywords.append(arg)
 
442
        elif opt in ('-K', '--no-default-keywords'):
 
443
            default_keywords = []
 
444
        elif opt in ('-n', '--add-location'):
 
445
            options.writelocations = 1
 
446
        elif opt in ('--no-location',):
 
447
            options.writelocations = 0
 
448
        elif opt in ('-S', '--style'):
 
449
            options.locationstyle = locations.get(arg.lower())
 
450
            if options.locationstyle is None:
 
451
                usage(1, _('Invalid value for --style: %s') % arg)
 
452
        elif opt in ('-o', '--output'):
 
453
            options.outfile = arg
 
454
        elif opt in ('-p', '--output-dir'):
 
455
            options.outpath = arg
 
456
        elif opt in ('-v', '--verbose'):
 
457
            options.verbose = 1
 
458
        elif opt in ('-V', '--version'):
 
459
            print _('pygettext.py (xgettext for Python) %s') % __version__
 
460
            sys.exit(0)
 
461
        elif opt in ('-w', '--width'):
 
462
            try:
 
463
                options.width = int(arg)
 
464
            except ValueError:
 
465
                usage(1, _('--width argument must be an integer: %s') % arg)
 
466
        elif opt in ('-x', '--exclude-file'):
 
467
            options.excludefilename = arg
 
468
        elif opt in ('-X', '--no-docstrings'):
 
469
            fp = open(arg)
 
470
            try:
 
471
                while 1:
 
472
                    line = fp.readline()
 
473
                    if not line:
 
474
                        break
 
475
                    options.nodocstrings[line[:-1]] = 1
 
476
            finally:
 
477
                fp.close()
 
478
 
 
479
    # calculate escapes
 
480
    make_escapes(options.escape)
 
481
 
 
482
    # calculate all keywords
 
483
    options.keywords.extend(default_keywords)
 
484
 
 
485
    # initialize list of strings to exclude
 
486
    if options.excludefilename:
 
487
        try:
 
488
            fp = open(options.excludefilename)
 
489
            options.toexclude = fp.readlines()
 
490
            fp.close()
 
491
        except IOError:
 
492
            print >> sys.stderr, _(
 
493
                "Can't read --exclude-file: %s") % options.excludefilename
 
494
            sys.exit(1)
 
495
    else:
 
496
        options.toexclude = []
 
497
 
 
498
    # slurp through all the files
 
499
    eater = TokenEater(options)
 
500
    for filename in args:
 
501
        if filename == '-':
 
502
            if options.verbose:
 
503
                print _('Reading standard input')
 
504
            fp = sys.stdin
 
505
            closep = 0
 
506
        else:
 
507
            if options.verbose:
 
508
                print _('Working on %s') % filename
 
509
            fp = open(filename)
 
510
            closep = 1
 
511
        try:
 
512
            eater.set_filename(filename)
 
513
            try:
 
514
                tokenize.tokenize(fp.readline, eater)
 
515
            except tokenize.TokenError, e:
 
516
                print >> sys.stderr, '%s: %s, line %d, column %d' % (
 
517
                    e[0], filename, e[1][0], e[1][1])
 
518
        finally:
 
519
            if closep:
 
520
                fp.close()
 
521
 
 
522
    # write the output
 
523
    if options.outfile == '-':
 
524
        fp = sys.stdout
 
525
        closep = 0
 
526
    else:
 
527
        if options.outpath:
 
528
            options.outfile = os.path.join(options.outpath, options.outfile)
 
529
        fp = open(options.outfile, 'w')
 
530
        closep = 1
 
531
    try:
 
532
        eater.write(fp)
 
533
    finally:
 
534
        if closep:
 
535
            fp.close()
 
536
 
 
537
 
 
538
if __name__ == '__main__':
 
539
    main()
 
540
    # some more test strings
 
541
    _(u'a unicode string')