Code

Add config-option "nosy" to messages_to_author setting in [nosy] section
[roundup.git] / roundup / cgi / TAL / talgettext.py
1 #!/usr/bin/env python
2 ##############################################################################
3 #
4 # Copyright (c) 2002 Zope Corporation and Contributors.
5 # All Rights Reserved.
6 #
7 # This software is subject to the provisions of the Zope Public License,
8 # Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
9 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
10 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
11 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
12 # FOR A PARTICULAR PURPOSE.
13 #
14 ##############################################################################
15 # Modifications for Roundup:
16 # 1. commented out ITALES references
17 # 2. escape quotes and line feeds in msgids
18 # 3. don't collect empty msgids
20 """Program to extract internationalization markup from Page Templates.
22 Once you have marked up a Page Template file with i18n: namespace tags, use
23 this program to extract GNU gettext .po file entries.
25 Usage: talgettext.py [options] files
26 Options:
27     -h / --help
28         Print this message and exit.
29     -o / --output <file>
30         Output the translation .po file to <file>.
31     -u / --update <file>
32         Update the existing translation <file> with any new translation strings
33         found.
34 """
36 import sys
37 import time
38 import getopt
39 import traceback
41 from roundup.cgi.TAL.HTMLTALParser import HTMLTALParser
42 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
43 from roundup.cgi.TAL.DummyEngine import DummyEngine
44 #from ITALES import ITALESEngine
45 from roundup.cgi.TAL.TALDefs import TALESError
47 __version__ = '$Revision: 1.6 $'
49 pot_header = '''\
50 # SOME DESCRIPTIVE TITLE.
51 # Copyright (C) YEAR ORGANIZATION
52 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
53 #
54 msgid ""
55 msgstr ""
56 "Project-Id-Version: PACKAGE VERSION\\n"
57 "POT-Creation-Date: %(time)s\\n"
58 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"
59 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"
60 "Language-Team: LANGUAGE <LL@li.org>\\n"
61 "MIME-Version: 1.0\\n"
62 "Content-Type: text/plain; charset=CHARSET\\n"
63 "Content-Transfer-Encoding: ENCODING\\n"
64 "Generated-By: talgettext.py %(version)s\\n"
65 '''
67 NLSTR = '"\n"'
69 try:
70     True
71 except NameError:
72     True=1
73     False=0
75 def usage(code, msg=''):
76     # Python 2.1 required
77     print >> sys.stderr, __doc__
78     if msg:
79         print >> sys.stderr, msg
80     sys.exit(code)
83 class POTALInterpreter(TALInterpreter):
84     def translate(self, msgid, default, i18ndict=None, obj=None):
85         # XXX is this right?
86         if i18ndict is None:
87             i18ndict = {}
88         if obj:
89             i18ndict.update(obj)
90         # XXX Mmmh, it seems that sometimes the msgid is None; is that really
91         # possible?
92         if msgid is None:
93             return None
94         # XXX We need to pass in one of context or target_language
95         return self.engine.translate(msgid, self.i18nContext.domain, i18ndict,
96                                      position=self.position, default=default)
99 class POEngine(DummyEngine):
100     #__implements__ = ITALESEngine
102     def __init__(self, macros=None):
103         self.catalog = {}
104         DummyEngine.__init__(self, macros)
106     def evaluate(*args):
107         return '' # who cares
109     def evaluatePathOrVar(*args):
110         return '' # who cares
112     def evaluateSequence(self, expr):
113         return (0,) # dummy
115     def evaluateBoolean(self, expr):
116         return True # dummy
118     def translate(self, msgid, domain=None, mapping=None, default=None,
119                   # XXX position is not part of the ITALESEngine
120                   #     interface
121                   position=None):
123         if not msgid: return 'x'
125         if domain not in self.catalog:
126             self.catalog[domain] = {}
127         domain = self.catalog[domain]
129         if msgid not in domain:
130             domain[msgid] = []
131         domain[msgid].append((self.file, position))
132         return 'x'
135 class UpdatePOEngine(POEngine):
136     """A slightly-less braindead POEngine which supports loading an existing
137     .po file first."""
139     def __init__ (self, macros=None, filename=None):
140         POEngine.__init__(self, macros)
142         self._filename = filename
143         self._loadFile()
144         self.base = self.catalog
145         self.catalog = {}
147     def __add(self, id, s, fuzzy):
148         "Add a non-fuzzy translation to the dictionary."
149         if not fuzzy and str:
150             # check for multi-line values and munge them appropriately
151             if '\n' in s:
152                 lines = s.rstrip().split('\n')
153                 s = NLSTR.join(lines)
154             self.catalog[id] = s
156     def _loadFile(self):
157         # shamelessly cribbed from Python's Tools/i18n/msgfmt.py
158         # 25-Mar-2003 Nathan R. Yergler (nathan@zope.org)
159         # 14-Apr-2003 Hacked by Barry Warsaw (barry@zope.com)
161         ID = 1
162         STR = 2
164         try:
165             lines = open(self._filename).readlines()
166         except IOError, msg:
167             print >> sys.stderr, msg
168             sys.exit(1)
170         section = None
171         fuzzy = False
173         # Parse the catalog
174         lno = 0
175         for l in lines:
176             lno += True
177             # If we get a comment line after a msgstr, this is a new entry
178             if l[0] == '#' and section == STR:
179                 self.__add(msgid, msgstr, fuzzy)
180                 section = None
181                 fuzzy = False
182             # Record a fuzzy mark
183             if l[:2] == '#,' and l.find('fuzzy'):
184                 fuzzy = True
185             # Skip comments
186             if l[0] == '#':
187                 continue
188             # Now we are in a msgid section, output previous section
189             if l.startswith('msgid'):
190                 if section == STR:
191                     self.__add(msgid, msgstr, fuzzy)
192                 section = ID
193                 l = l[5:]
194                 msgid = msgstr = ''
195             # Now we are in a msgstr section
196             elif l.startswith('msgstr'):
197                 section = STR
198                 l = l[6:]
199             # Skip empty lines
200             if not l.strip():
201                 continue
202             # XXX: Does this always follow Python escape semantics?
203             l = eval(l)
204             if section == ID:
205                 msgid += l
206             elif section == STR:
207                 msgstr += '%s\n' % l
208             else:
209                 print >> sys.stderr, 'Syntax error on %s:%d' % (infile, lno), \
210                       'before:'
211                 print >> sys.stderr, l
212                 sys.exit(1)
213         # Add last entry
214         if section == STR:
215             self.__add(msgid, msgstr, fuzzy)
217     def evaluate(self, expression):
218         try:
219             return POEngine.evaluate(self, expression)
220         except TALESError:
221             pass
223     def evaluatePathOrVar(self, expr):
224         return 'who cares'
226     def translate(self, msgid, domain=None, mapping=None, default=None,
227                   position=None):
228         if msgid not in self.base:
229             POEngine.translate(self, msgid, domain, mapping, default, position)
230         return 'x'
233 def main():
234     try:
235         opts, args = getopt.getopt(
236             sys.argv[1:],
237             'ho:u:',
238             ['help', 'output=', 'update='])
239     except getopt.error, msg:
240         usage(1, msg)
242     outfile = None
243     engine = None
244     update_mode = False
245     for opt, arg in opts:
246         if opt in ('-h', '--help'):
247             usage(0)
248         elif opt in ('-o', '--output'):
249             outfile = arg
250         elif opt in ('-u', '--update'):
251             update_mode = True
252             if outfile is None:
253                 outfile = arg
254             engine = UpdatePOEngine(filename=arg)
256     if not args:
257         print 'nothing to do'
258         return
260     # We don't care about the rendered output of the .pt file
261     class Devnull:
262         def write(self, s):
263             pass
265     # check if we've already instantiated an engine;
266     # if not, use the stupidest one available
267     if not engine:
268         engine = POEngine()
270     # process each file specified
271     for filename in args:
272         try:
273             engine.file = filename
274             p = HTMLTALParser()
275             p.parseFile(filename)
276             program, macros = p.getCode()
277             POTALInterpreter(program, macros, engine, stream=Devnull(),
278                              metal=False)()
279         except: # Hee hee, I love bare excepts!
280             print 'There was an error processing', filename
281             traceback.print_exc()
283     # Now output the keys in the engine.  Write them to a file if --output or
284     # --update was specified; otherwise use standard out.
285     if (outfile is None):
286         outfile = sys.stdout
287     else:
288         outfile = file(outfile, update_mode and "a" or "w")
290     catalog = {}
291     for domain in engine.catalog.keys():
292         catalog.update(engine.catalog[domain])
294     messages = catalog.copy()
295     try:
296         messages.update(engine.base)
297     except AttributeError:
298         pass
299     if '' not in messages:
300         print >> outfile, pot_header % {'time': time.ctime(),
301                                         'version': __version__}
303     msgids = catalog.keys()
304     # XXX: You should not sort by msgid, but by filename and position. (SR)
305     msgids.sort()
306     for msgid in msgids:
307         positions = catalog[msgid]
308         for filename, position in positions:
309             outfile.write('#: %s:%s\n' % (filename, position[0]))
311         outfile.write('msgid "%s"\n'
312             % msgid.replace('"', '\\"').replace("\n", '\\n"\n"'))
313         outfile.write('msgstr ""\n')
314         outfile.write('\n')
317 if __name__ == '__main__':
318     main()