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