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