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()