1 | #!/usr/bin/env python |
---|
2 | # -*- coding: utf-8 -*- |
---|
3 | # |
---|
4 | # Copyright (C) 2007 Edgewall Software |
---|
5 | # All rights reserved. |
---|
6 | # |
---|
7 | # This software is licensed as described in the file COPYING, which |
---|
8 | # you should have received as part of this distribution. The terms |
---|
9 | # are also available at http://babel.edgewall.org/wiki/License. |
---|
10 | # |
---|
11 | # This software consists of voluntary contributions made by many |
---|
12 | # individuals. For the exact contribution history, see the revision |
---|
13 | # history and logs, available at http://babel.edgewall.org/log/. |
---|
14 | |
---|
15 | """Frontends for the message extraction functionality.""" |
---|
16 | |
---|
17 | from ConfigParser import RawConfigParser |
---|
18 | from datetime import datetime |
---|
19 | from distutils import log |
---|
20 | from distutils.cmd import Command |
---|
21 | from distutils.errors import DistutilsOptionError, DistutilsSetupError |
---|
22 | from locale import getpreferredencoding |
---|
23 | import logging |
---|
24 | from optparse import OptionParser |
---|
25 | import os |
---|
26 | import re |
---|
27 | import shutil |
---|
28 | from StringIO import StringIO |
---|
29 | import sys |
---|
30 | import tempfile |
---|
31 | |
---|
32 | from babel import __version__ as VERSION |
---|
33 | from babel import Locale, localedata |
---|
34 | from babel.core import UnknownLocaleError |
---|
35 | from babel.messages.catalog import Catalog |
---|
36 | from babel.messages.extract import extract_from_dir, DEFAULT_KEYWORDS, \ |
---|
37 | DEFAULT_MAPPING |
---|
38 | from babel.messages.mofile import write_mo |
---|
39 | from babel.messages.pofile import read_po, write_po |
---|
40 | from babel.messages.plurals import PLURALS |
---|
41 | from babel.util import odict, LOCALTZ |
---|
42 | |
---|
43 | __all__ = ['CommandLineInterface', 'compile_catalog', 'extract_messages', |
---|
44 | 'init_catalog', 'check_message_extractors', 'update_catalog'] |
---|
45 | __docformat__ = 'restructuredtext en' |
---|
46 | |
---|
47 | |
---|
48 | class compile_catalog(Command): |
---|
49 | """Catalog compilation command for use in ``setup.py`` scripts. |
---|
50 | |
---|
51 | If correctly installed, this command is available to Setuptools-using |
---|
52 | setup scripts automatically. For projects using plain old ``distutils``, |
---|
53 | the command needs to be registered explicitly in ``setup.py``:: |
---|
54 | |
---|
55 | from babel.messages.frontend import compile_catalog |
---|
56 | |
---|
57 | setup( |
---|
58 | ... |
---|
59 | cmdclass = {'compile_catalog': compile_catalog} |
---|
60 | ) |
---|
61 | |
---|
62 | :since: version 0.9 |
---|
63 | :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_ |
---|
64 | :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_ |
---|
65 | """ |
---|
66 | |
---|
67 | description = 'compile message catalogs to binary MO files' |
---|
68 | user_options = [ |
---|
69 | ('domain=', 'D', |
---|
70 | "domain of PO file (default 'messages')"), |
---|
71 | ('directory=', 'd', |
---|
72 | 'path to base directory containing the catalogs'), |
---|
73 | ('input-file=', 'i', |
---|
74 | 'name of the input file'), |
---|
75 | ('output-file=', 'o', |
---|
76 | "name of the output file (default " |
---|
77 | "'<output_dir>/<locale>/LC_MESSAGES/<domain>.po')"), |
---|
78 | ('locale=', 'l', |
---|
79 | 'locale of the catalog to compile'), |
---|
80 | ('use-fuzzy', 'f', |
---|
81 | 'also include fuzzy translations'), |
---|
82 | ('statistics', None, |
---|
83 | 'print statistics about translations') |
---|
84 | ] |
---|
85 | boolean_options = ['use-fuzzy', 'statistics'] |
---|
86 | |
---|
87 | def initialize_options(self): |
---|
88 | self.domain = 'messages' |
---|
89 | self.directory = None |
---|
90 | self.input_file = None |
---|
91 | self.output_file = None |
---|
92 | self.locale = None |
---|
93 | self.use_fuzzy = False |
---|
94 | self.statistics = False |
---|
95 | |
---|
96 | def finalize_options(self): |
---|
97 | if not self.input_file and not self.directory: |
---|
98 | raise DistutilsOptionError('you must specify either the input file ' |
---|
99 | 'or the base directory') |
---|
100 | if not self.output_file and not self.directory: |
---|
101 | raise DistutilsOptionError('you must specify either the input file ' |
---|
102 | 'or the base directory') |
---|
103 | |
---|
104 | def run(self): |
---|
105 | po_files = [] |
---|
106 | mo_files = [] |
---|
107 | |
---|
108 | if not self.input_file: |
---|
109 | if self.locale: |
---|
110 | po_files.append((self.locale, |
---|
111 | os.path.join(self.directory, self.locale, |
---|
112 | 'LC_MESSAGES', |
---|
113 | self.domain + '.po'))) |
---|
114 | mo_files.append(os.path.join(self.directory, self.locale, |
---|
115 | 'LC_MESSAGES', |
---|
116 | self.domain + '.mo')) |
---|
117 | else: |
---|
118 | for locale in os.listdir(self.directory): |
---|
119 | po_file = os.path.join(self.directory, locale, |
---|
120 | 'LC_MESSAGES', self.domain + '.po') |
---|
121 | if os.path.exists(po_file): |
---|
122 | po_files.append((locale, po_file)) |
---|
123 | mo_files.append(os.path.join(self.directory, locale, |
---|
124 | 'LC_MESSAGES', |
---|
125 | self.domain + '.mo')) |
---|
126 | else: |
---|
127 | po_files.append((self.locale, self.input_file)) |
---|
128 | if self.output_file: |
---|
129 | mo_files.append(self.output_file) |
---|
130 | else: |
---|
131 | mo_files.append(os.path.join(self.directory, self.locale, |
---|
132 | 'LC_MESSAGES', |
---|
133 | self.domain + '.mo')) |
---|
134 | |
---|
135 | if not po_files: |
---|
136 | raise DistutilsOptionError('no message catalogs found') |
---|
137 | |
---|
138 | for idx, (locale, po_file) in enumerate(po_files): |
---|
139 | mo_file = mo_files[idx] |
---|
140 | infile = open(po_file, 'r') |
---|
141 | try: |
---|
142 | catalog = read_po(infile, locale) |
---|
143 | finally: |
---|
144 | infile.close() |
---|
145 | |
---|
146 | if self.statistics: |
---|
147 | translated = 0 |
---|
148 | for message in list(catalog)[1:]: |
---|
149 | if message.string: |
---|
150 | translated +=1 |
---|
151 | percentage = 0 |
---|
152 | if len(catalog): |
---|
153 | percentage = translated * 100 // len(catalog) |
---|
154 | log.info('%d of %d messages (%d%%) translated in %r', |
---|
155 | translated, len(catalog), percentage, po_file) |
---|
156 | |
---|
157 | if catalog.fuzzy and not self.use_fuzzy: |
---|
158 | log.warn('catalog %r is marked as fuzzy, skipping', po_file) |
---|
159 | continue |
---|
160 | |
---|
161 | for message, errors in catalog.check(): |
---|
162 | for error in errors: |
---|
163 | log.error('error: %s:%d: %s', po_file, message.lineno, |
---|
164 | error) |
---|
165 | |
---|
166 | log.info('compiling catalog %r to %r', po_file, mo_file) |
---|
167 | |
---|
168 | outfile = open(mo_file, 'wb') |
---|
169 | try: |
---|
170 | write_mo(outfile, catalog, use_fuzzy=self.use_fuzzy) |
---|
171 | finally: |
---|
172 | outfile.close() |
---|
173 | |
---|
174 | |
---|
175 | class extract_messages(Command): |
---|
176 | """Message extraction command for use in ``setup.py`` scripts. |
---|
177 | |
---|
178 | If correctly installed, this command is available to Setuptools-using |
---|
179 | setup scripts automatically. For projects using plain old ``distutils``, |
---|
180 | the command needs to be registered explicitly in ``setup.py``:: |
---|
181 | |
---|
182 | from babel.messages.frontend import extract_messages |
---|
183 | |
---|
184 | setup( |
---|
185 | ... |
---|
186 | cmdclass = {'extract_messages': extract_messages} |
---|
187 | ) |
---|
188 | |
---|
189 | :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_ |
---|
190 | :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_ |
---|
191 | """ |
---|
192 | |
---|
193 | description = 'extract localizable strings from the project code' |
---|
194 | user_options = [ |
---|
195 | ('charset=', None, |
---|
196 | 'charset to use in the output file'), |
---|
197 | ('keywords=', 'k', |
---|
198 | 'space-separated list of keywords to look for in addition to the ' |
---|
199 | 'defaults'), |
---|
200 | ('no-default-keywords', None, |
---|
201 | 'do not include the default keywords'), |
---|
202 | ('mapping-file=', 'F', |
---|
203 | 'path to the mapping configuration file'), |
---|
204 | ('no-location', None, |
---|
205 | 'do not include location comments with filename and line number'), |
---|
206 | ('omit-header', None, |
---|
207 | 'do not include msgid "" entry in header'), |
---|
208 | ('output-file=', 'o', |
---|
209 | 'name of the output file'), |
---|
210 | ('width=', 'w', |
---|
211 | 'set output line width (default 76)'), |
---|
212 | ('no-wrap', None, |
---|
213 | 'do not break long message lines, longer than the output line width, ' |
---|
214 | 'into several lines'), |
---|
215 | ('sort-output', None, |
---|
216 | 'generate sorted output (default False)'), |
---|
217 | ('sort-by-file', None, |
---|
218 | 'sort output by file location (default False)'), |
---|
219 | ('msgid-bugs-address=', None, |
---|
220 | 'set report address for msgid'), |
---|
221 | ('copyright-holder=', None, |
---|
222 | 'set copyright holder in output'), |
---|
223 | ('add-comments=', 'c', |
---|
224 | 'place comment block with TAG (or those preceding keyword lines) in ' |
---|
225 | 'output file. Seperate multiple TAGs with commas(,)'), |
---|
226 | ('strip-comments', None, |
---|
227 | 'strip the comment TAGs from the comments.'), |
---|
228 | ('input-dirs=', None, |
---|
229 | 'directories that should be scanned for messages'), |
---|
230 | ] |
---|
231 | boolean_options = [ |
---|
232 | 'no-default-keywords', 'no-location', 'omit-header', 'no-wrap', |
---|
233 | 'sort-output', 'sort-by-file', 'strip-comments' |
---|
234 | ] |
---|
235 | |
---|
236 | def initialize_options(self): |
---|
237 | self.charset = 'utf-8' |
---|
238 | self.keywords = '' |
---|
239 | self._keywords = DEFAULT_KEYWORDS.copy() |
---|
240 | self.no_default_keywords = False |
---|
241 | self.mapping_file = None |
---|
242 | self.no_location = False |
---|
243 | self.omit_header = False |
---|
244 | self.output_file = None |
---|
245 | self.input_dirs = None |
---|
246 | self.width = 76 |
---|
247 | self.no_wrap = False |
---|
248 | self.sort_output = False |
---|
249 | self.sort_by_file = False |
---|
250 | self.msgid_bugs_address = None |
---|
251 | self.copyright_holder = None |
---|
252 | self.add_comments = None |
---|
253 | self._add_comments = [] |
---|
254 | self.strip_comments = False |
---|
255 | |
---|
256 | def finalize_options(self): |
---|
257 | if self.no_default_keywords and not self.keywords: |
---|
258 | raise DistutilsOptionError('you must specify new keywords if you ' |
---|
259 | 'disable the default ones') |
---|
260 | if self.no_default_keywords: |
---|
261 | self._keywords = {} |
---|
262 | if self.keywords: |
---|
263 | self._keywords.update(parse_keywords(self.keywords.split())) |
---|
264 | |
---|
265 | if not self.output_file: |
---|
266 | raise DistutilsOptionError('no output file specified') |
---|
267 | if self.no_wrap and self.width: |
---|
268 | raise DistutilsOptionError("'--no-wrap' and '--width' are mutually " |
---|
269 | "exclusive") |
---|
270 | if self.no_wrap: |
---|
271 | self.width = None |
---|
272 | else: |
---|
273 | self.width = int(self.width) |
---|
274 | |
---|
275 | if self.sort_output and self.sort_by_file: |
---|
276 | raise DistutilsOptionError("'--sort-output' and '--sort-by-file' " |
---|
277 | "are mutually exclusive") |
---|
278 | |
---|
279 | if not self.input_dirs: |
---|
280 | self.input_dirs = dict.fromkeys([k.split('.',1)[0] |
---|
281 | for k in self.distribution.packages |
---|
282 | ]).keys() |
---|
283 | |
---|
284 | if self.add_comments: |
---|
285 | self._add_comments = self.add_comments.split(',') |
---|
286 | |
---|
287 | def run(self): |
---|
288 | mappings = self._get_mappings() |
---|
289 | outfile = open(self.output_file, 'w') |
---|
290 | try: |
---|
291 | catalog = Catalog(project=self.distribution.get_name(), |
---|
292 | version=self.distribution.get_version(), |
---|
293 | msgid_bugs_address=self.msgid_bugs_address, |
---|
294 | copyright_holder=self.copyright_holder, |
---|
295 | charset=self.charset) |
---|
296 | |
---|
297 | for dirname, (method_map, options_map) in mappings.items(): |
---|
298 | def callback(filename, method, options): |
---|
299 | if method == 'ignore': |
---|
300 | return |
---|
301 | filepath = os.path.normpath(os.path.join(dirname, filename)) |
---|
302 | optstr = '' |
---|
303 | if options: |
---|
304 | optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for |
---|
305 | k, v in options.items()]) |
---|
306 | log.info('extracting messages from %s%s', filepath, optstr) |
---|
307 | |
---|
308 | extracted = extract_from_dir(dirname, method_map, options_map, |
---|
309 | keywords=self._keywords, |
---|
310 | comment_tags=self._add_comments, |
---|
311 | callback=callback, |
---|
312 | strip_comment_tags= |
---|
313 | self.strip_comments) |
---|
314 | for filename, lineno, message, comments in extracted: |
---|
315 | filepath = os.path.normpath(os.path.join(dirname, filename)) |
---|
316 | catalog.add(message, None, [(filepath, lineno)], |
---|
317 | auto_comments=comments) |
---|
318 | |
---|
319 | log.info('writing PO template file to %s' % self.output_file) |
---|
320 | write_po(outfile, catalog, width=self.width, |
---|
321 | no_location=self.no_location, |
---|
322 | omit_header=self.omit_header, |
---|
323 | sort_output=self.sort_output, |
---|
324 | sort_by_file=self.sort_by_file) |
---|
325 | finally: |
---|
326 | outfile.close() |
---|
327 | |
---|
328 | def _get_mappings(self): |
---|
329 | mappings = {} |
---|
330 | |
---|
331 | if self.mapping_file: |
---|
332 | fileobj = open(self.mapping_file, 'U') |
---|
333 | try: |
---|
334 | method_map, options_map = parse_mapping(fileobj) |
---|
335 | for dirname in self.input_dirs: |
---|
336 | mappings[dirname] = method_map, options_map |
---|
337 | finally: |
---|
338 | fileobj.close() |
---|
339 | |
---|
340 | elif getattr(self.distribution, 'message_extractors', None): |
---|
341 | message_extractors = self.distribution.message_extractors |
---|
342 | for dirname, mapping in message_extractors.items(): |
---|
343 | if isinstance(mapping, basestring): |
---|
344 | method_map, options_map = parse_mapping(StringIO(mapping)) |
---|
345 | else: |
---|
346 | method_map, options_map = [], {} |
---|
347 | for pattern, method, options in mapping: |
---|
348 | method_map.append((pattern, method)) |
---|
349 | options_map[pattern] = options or {} |
---|
350 | mappings[dirname] = method_map, options_map |
---|
351 | |
---|
352 | else: |
---|
353 | for dirname in self.input_dirs: |
---|
354 | mappings[dirname] = DEFAULT_MAPPING, {} |
---|
355 | |
---|
356 | return mappings |
---|
357 | |
---|
358 | |
---|
359 | def check_message_extractors(dist, name, value): |
---|
360 | """Validate the ``message_extractors`` keyword argument to ``setup()``. |
---|
361 | |
---|
362 | :param dist: the distutils/setuptools ``Distribution`` object |
---|
363 | :param name: the name of the keyword argument (should always be |
---|
364 | "message_extractors") |
---|
365 | :param value: the value of the keyword argument |
---|
366 | :raise `DistutilsSetupError`: if the value is not valid |
---|
367 | :see: `Adding setup() arguments |
---|
368 | <http://peak.telecommunity.com/DevCenter/setuptools#adding-setup-arguments>`_ |
---|
369 | """ |
---|
370 | assert name == 'message_extractors' |
---|
371 | if not isinstance(value, dict): |
---|
372 | raise DistutilsSetupError('the value of the "message_extractors" ' |
---|
373 | 'parameter must be a dictionary') |
---|
374 | |
---|
375 | |
---|
376 | class init_catalog(Command): |
---|
377 | """New catalog initialization command for use in ``setup.py`` scripts. |
---|
378 | |
---|
379 | If correctly installed, this command is available to Setuptools-using |
---|
380 | setup scripts automatically. For projects using plain old ``distutils``, |
---|
381 | the command needs to be registered explicitly in ``setup.py``:: |
---|
382 | |
---|
383 | from babel.messages.frontend import init_catalog |
---|
384 | |
---|
385 | setup( |
---|
386 | ... |
---|
387 | cmdclass = {'init_catalog': init_catalog} |
---|
388 | ) |
---|
389 | |
---|
390 | :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_ |
---|
391 | :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_ |
---|
392 | """ |
---|
393 | |
---|
394 | description = 'create a new catalog based on a POT file' |
---|
395 | user_options = [ |
---|
396 | ('domain=', 'D', |
---|
397 | "domain of PO file (default 'messages')"), |
---|
398 | ('input-file=', 'i', |
---|
399 | 'name of the input file'), |
---|
400 | ('output-dir=', 'd', |
---|
401 | 'path to output directory'), |
---|
402 | ('output-file=', 'o', |
---|
403 | "name of the output file (default " |
---|
404 | "'<output_dir>/<locale>/LC_MESSAGES/<domain>.po')"), |
---|
405 | ('locale=', 'l', |
---|
406 | 'locale for the new localized catalog'), |
---|
407 | ] |
---|
408 | |
---|
409 | def initialize_options(self): |
---|
410 | self.output_dir = None |
---|
411 | self.output_file = None |
---|
412 | self.input_file = None |
---|
413 | self.locale = None |
---|
414 | self.domain = 'messages' |
---|
415 | |
---|
416 | def finalize_options(self): |
---|
417 | if not self.input_file: |
---|
418 | raise DistutilsOptionError('you must specify the input file') |
---|
419 | |
---|
420 | if not self.locale: |
---|
421 | raise DistutilsOptionError('you must provide a locale for the ' |
---|
422 | 'new catalog') |
---|
423 | try: |
---|
424 | self._locale = Locale.parse(self.locale) |
---|
425 | except UnknownLocaleError, e: |
---|
426 | raise DistutilsOptionError(e) |
---|
427 | |
---|
428 | if not self.output_file and not self.output_dir: |
---|
429 | raise DistutilsOptionError('you must specify the output directory') |
---|
430 | if not self.output_file: |
---|
431 | self.output_file = os.path.join(self.output_dir, self.locale, |
---|
432 | 'LC_MESSAGES', self.domain + '.po') |
---|
433 | |
---|
434 | if not os.path.exists(os.path.dirname(self.output_file)): |
---|
435 | os.makedirs(os.path.dirname(self.output_file)) |
---|
436 | |
---|
437 | def run(self): |
---|
438 | log.info('creating catalog %r based on %r', self.output_file, |
---|
439 | self.input_file) |
---|
440 | |
---|
441 | infile = open(self.input_file, 'r') |
---|
442 | try: |
---|
443 | # Although reading from the catalog template, read_po must be fed |
---|
444 | # the locale in order to correcly calculate plurals |
---|
445 | catalog = read_po(infile, locale=self.locale) |
---|
446 | finally: |
---|
447 | infile.close() |
---|
448 | |
---|
449 | catalog.locale = self._locale |
---|
450 | catalog.fuzzy = False |
---|
451 | |
---|
452 | outfile = open(self.output_file, 'w') |
---|
453 | try: |
---|
454 | write_po(outfile, catalog) |
---|
455 | finally: |
---|
456 | outfile.close() |
---|
457 | |
---|
458 | |
---|
459 | class update_catalog(Command): |
---|
460 | """Catalog merging command for use in ``setup.py`` scripts. |
---|
461 | |
---|
462 | If correctly installed, this command is available to Setuptools-using |
---|
463 | setup scripts automatically. For projects using plain old ``distutils``, |
---|
464 | the command needs to be registered explicitly in ``setup.py``:: |
---|
465 | |
---|
466 | from babel.messages.frontend import update_catalog |
---|
467 | |
---|
468 | setup( |
---|
469 | ... |
---|
470 | cmdclass = {'update_catalog': update_catalog} |
---|
471 | ) |
---|
472 | |
---|
473 | :since: version 0.9 |
---|
474 | :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_ |
---|
475 | :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_ |
---|
476 | """ |
---|
477 | |
---|
478 | description = 'update message catalogs from a POT file' |
---|
479 | user_options = [ |
---|
480 | ('domain=', 'D', |
---|
481 | "domain of PO file (default 'messages')"), |
---|
482 | ('input-file=', 'i', |
---|
483 | 'name of the input file'), |
---|
484 | ('output-dir=', 'd', |
---|
485 | 'path to base directory containing the catalogs'), |
---|
486 | ('output-file=', 'o', |
---|
487 | "name of the output file (default " |
---|
488 | "'<output_dir>/<locale>/LC_MESSAGES/<domain>.po')"), |
---|
489 | ('locale=', 'l', |
---|
490 | 'locale of the catalog to compile'), |
---|
491 | ('ignore-obsolete=', None, |
---|
492 | 'whether to omit obsolete messages from the output'), |
---|
493 | ('no-fuzzy-matching', 'N', |
---|
494 | 'do not use fuzzy matching'), |
---|
495 | ('previous', None, |
---|
496 | 'keep previous msgids of translated messages') |
---|
497 | ] |
---|
498 | boolean_options = ['ignore_obsolete', 'no_fuzzy_matching', 'previous'] |
---|
499 | |
---|
500 | def initialize_options(self): |
---|
501 | self.domain = 'messages' |
---|
502 | self.input_file = None |
---|
503 | self.output_dir = None |
---|
504 | self.output_file = None |
---|
505 | self.locale = None |
---|
506 | self.ignore_obsolete = False |
---|
507 | self.no_fuzzy_matching = False |
---|
508 | self.previous = False |
---|
509 | |
---|
510 | def finalize_options(self): |
---|
511 | if not self.input_file: |
---|
512 | raise DistutilsOptionError('you must specify the input file') |
---|
513 | if not self.output_file and not self.output_dir: |
---|
514 | raise DistutilsOptionError('you must specify the output file or ' |
---|
515 | 'directory') |
---|
516 | if self.output_file and not self.locale: |
---|
517 | raise DistutilsOptionError('you must specify the locale') |
---|
518 | if self.no_fuzzy_matching and self.previous: |
---|
519 | self.previous = False |
---|
520 | |
---|
521 | def run(self): |
---|
522 | po_files = [] |
---|
523 | if not self.output_file: |
---|
524 | if self.locale: |
---|
525 | po_files.append((self.locale, |
---|
526 | os.path.join(self.output_dir, self.locale, |
---|
527 | 'LC_MESSAGES', |
---|
528 | self.domain + '.po'))) |
---|
529 | else: |
---|
530 | for locale in os.listdir(self.output_dir): |
---|
531 | po_file = os.path.join(self.output_dir, locale, |
---|
532 | 'LC_MESSAGES', |
---|
533 | self.domain + '.po') |
---|
534 | if os.path.exists(po_file): |
---|
535 | po_files.append((locale, po_file)) |
---|
536 | else: |
---|
537 | po_files.append((self.locale, self.output_file)) |
---|
538 | |
---|
539 | domain = self.domain |
---|
540 | if not domain: |
---|
541 | domain = os.path.splitext(os.path.basename(self.input_file))[0] |
---|
542 | |
---|
543 | infile = open(self.input_file, 'U') |
---|
544 | try: |
---|
545 | template = read_po(infile) |
---|
546 | finally: |
---|
547 | infile.close() |
---|
548 | |
---|
549 | if not po_files: |
---|
550 | raise DistutilsOptionError('no message catalogs found') |
---|
551 | |
---|
552 | for locale, filename in po_files: |
---|
553 | log.info('updating catalog %r based on %r', filename, |
---|
554 | self.input_file) |
---|
555 | infile = open(filename, 'U') |
---|
556 | try: |
---|
557 | catalog = read_po(infile, locale=locale, domain=domain) |
---|
558 | finally: |
---|
559 | infile.close() |
---|
560 | |
---|
561 | catalog.update(template, self.no_fuzzy_matching) |
---|
562 | |
---|
563 | tmpname = os.path.join(os.path.dirname(filename), |
---|
564 | tempfile.gettempprefix() + |
---|
565 | os.path.basename(filename)) |
---|
566 | tmpfile = open(tmpname, 'w') |
---|
567 | try: |
---|
568 | try: |
---|
569 | write_po(tmpfile, catalog, |
---|
570 | ignore_obsolete=self.ignore_obsolete, |
---|
571 | include_previous=self.previous) |
---|
572 | finally: |
---|
573 | tmpfile.close() |
---|
574 | except: |
---|
575 | os.remove(tmpname) |
---|
576 | raise |
---|
577 | |
---|
578 | try: |
---|
579 | os.rename(tmpname, filename) |
---|
580 | except OSError: |
---|
581 | # We're probably on Windows, which doesn't support atomic |
---|
582 | # renames, at least not through Python |
---|
583 | # If the error is in fact due to a permissions problem, that |
---|
584 | # same error is going to be raised from one of the following |
---|
585 | # operations |
---|
586 | os.remove(filename) |
---|
587 | shutil.copy(tmpname, filename) |
---|
588 | os.remove(tmpname) |
---|
589 | |
---|
590 | |
---|
591 | class CommandLineInterface(object): |
---|
592 | """Command-line interface. |
---|
593 | |
---|
594 | This class provides a simple command-line interface to the message |
---|
595 | extraction and PO file generation functionality. |
---|
596 | """ |
---|
597 | |
---|
598 | usage = '%%prog %s [options] %s' |
---|
599 | version = '%%prog %s' % VERSION |
---|
600 | commands = { |
---|
601 | 'compile': 'compile message catalogs to MO files', |
---|
602 | 'extract': 'extract messages from source files and generate a POT file', |
---|
603 | 'init': 'create new message catalogs from a POT file', |
---|
604 | 'update': 'update existing message catalogs from a POT file' |
---|
605 | } |
---|
606 | |
---|
607 | def run(self, argv=sys.argv): |
---|
608 | """Main entry point of the command-line interface. |
---|
609 | |
---|
610 | :param argv: list of arguments passed on the command-line |
---|
611 | """ |
---|
612 | self.parser = OptionParser(usage=self.usage % ('command', '[args]'), |
---|
613 | version=self.version) |
---|
614 | self.parser.disable_interspersed_args() |
---|
615 | self.parser.print_help = self._help |
---|
616 | self.parser.add_option('--list-locales', dest='list_locales', |
---|
617 | action='store_true', |
---|
618 | help="print all known locales and exit") |
---|
619 | self.parser.add_option('-v', '--verbose', action='store_const', |
---|
620 | dest='loglevel', const=logging.DEBUG, |
---|
621 | help='print as much as possible') |
---|
622 | self.parser.add_option('-q', '--quiet', action='store_const', |
---|
623 | dest='loglevel', const=logging.ERROR, |
---|
624 | help='print as little as possible') |
---|
625 | self.parser.set_defaults(list_locales=False, loglevel=logging.INFO) |
---|
626 | |
---|
627 | options, args = self.parser.parse_args(argv[1:]) |
---|
628 | |
---|
629 | # Configure logging |
---|
630 | self.log = logging.getLogger('babel') |
---|
631 | self.log.setLevel(options.loglevel) |
---|
632 | handler = logging.StreamHandler() |
---|
633 | handler.setLevel(options.loglevel) |
---|
634 | formatter = logging.Formatter('%(message)s') |
---|
635 | handler.setFormatter(formatter) |
---|
636 | self.log.addHandler(handler) |
---|
637 | |
---|
638 | if options.list_locales: |
---|
639 | identifiers = localedata.list() |
---|
640 | longest = max([len(identifier) for identifier in identifiers]) |
---|
641 | format = u'%%-%ds %%s' % (longest + 1) |
---|
642 | for identifier in localedata.list(): |
---|
643 | locale = Locale.parse(identifier) |
---|
644 | output = format % (identifier, locale.english_name) |
---|
645 | print output.encode(sys.stdout.encoding or |
---|
646 | getpreferredencoding() or |
---|
647 | 'ascii', 'replace') |
---|
648 | return 0 |
---|
649 | |
---|
650 | if not args: |
---|
651 | self.parser.error('incorrect number of arguments') |
---|
652 | |
---|
653 | cmdname = args[0] |
---|
654 | if cmdname not in self.commands: |
---|
655 | self.parser.error('unknown command "%s"' % cmdname) |
---|
656 | |
---|
657 | return getattr(self, cmdname)(args[1:]) |
---|
658 | |
---|
659 | def _help(self): |
---|
660 | print self.parser.format_help() |
---|
661 | print "commands:" |
---|
662 | longest = max([len(command) for command in self.commands]) |
---|
663 | format = " %%-%ds %%s" % max(8, longest + 1) |
---|
664 | commands = self.commands.items() |
---|
665 | commands.sort() |
---|
666 | for name, description in commands: |
---|
667 | print format % (name, description) |
---|
668 | |
---|
669 | def compile(self, argv): |
---|
670 | """Subcommand for compiling a message catalog to a MO file. |
---|
671 | |
---|
672 | :param argv: the command arguments |
---|
673 | :since: version 0.9 |
---|
674 | """ |
---|
675 | parser = OptionParser(usage=self.usage % ('compile', ''), |
---|
676 | description=self.commands['compile']) |
---|
677 | parser.add_option('--domain', '-D', dest='domain', |
---|
678 | help="domain of MO and PO files (default '%default')") |
---|
679 | parser.add_option('--directory', '-d', dest='directory', |
---|
680 | metavar='DIR', help='base directory of catalog files') |
---|
681 | parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', |
---|
682 | help='locale of the catalog') |
---|
683 | parser.add_option('--input-file', '-i', dest='input_file', |
---|
684 | metavar='FILE', help='name of the input file') |
---|
685 | parser.add_option('--output-file', '-o', dest='output_file', |
---|
686 | metavar='FILE', |
---|
687 | help="name of the output file (default " |
---|
688 | "'<output_dir>/<locale>/LC_MESSAGES/" |
---|
689 | "<domain>.mo')") |
---|
690 | parser.add_option('--use-fuzzy', '-f', dest='use_fuzzy', |
---|
691 | action='store_true', |
---|
692 | help='also include fuzzy translations (default ' |
---|
693 | '%default)') |
---|
694 | parser.add_option('--statistics', dest='statistics', |
---|
695 | action='store_true', |
---|
696 | help='print statistics about translations') |
---|
697 | |
---|
698 | parser.set_defaults(domain='messages', use_fuzzy=False, |
---|
699 | compile_all=False, statistics=False) |
---|
700 | options, args = parser.parse_args(argv) |
---|
701 | |
---|
702 | po_files = [] |
---|
703 | mo_files = [] |
---|
704 | if not options.input_file: |
---|
705 | if not options.directory: |
---|
706 | parser.error('you must specify either the input file or the ' |
---|
707 | 'base directory') |
---|
708 | if options.locale: |
---|
709 | po_files.append((options.locale, |
---|
710 | os.path.join(options.directory, |
---|
711 | options.locale, 'LC_MESSAGES', |
---|
712 | options.domain + '.po'))) |
---|
713 | mo_files.append(os.path.join(options.directory, options.locale, |
---|
714 | 'LC_MESSAGES', |
---|
715 | options.domain + '.mo')) |
---|
716 | else: |
---|
717 | for locale in os.listdir(options.directory): |
---|
718 | po_file = os.path.join(options.directory, locale, |
---|
719 | 'LC_MESSAGES', options.domain + '.po') |
---|
720 | if os.path.exists(po_file): |
---|
721 | po_files.append((locale, po_file)) |
---|
722 | mo_files.append(os.path.join(options.directory, locale, |
---|
723 | 'LC_MESSAGES', |
---|
724 | options.domain + '.mo')) |
---|
725 | else: |
---|
726 | po_files.append((options.locale, options.input_file)) |
---|
727 | if options.output_file: |
---|
728 | mo_files.append(options.output_file) |
---|
729 | else: |
---|
730 | if not options.directory: |
---|
731 | parser.error('you must specify either the input file or ' |
---|
732 | 'the base directory') |
---|
733 | mo_files.append(os.path.join(options.directory, options.locale, |
---|
734 | 'LC_MESSAGES', |
---|
735 | options.domain + '.mo')) |
---|
736 | if not po_files: |
---|
737 | parser.error('no message catalogs found') |
---|
738 | |
---|
739 | for idx, (locale, po_file) in enumerate(po_files): |
---|
740 | mo_file = mo_files[idx] |
---|
741 | infile = open(po_file, 'r') |
---|
742 | try: |
---|
743 | catalog = read_po(infile, locale) |
---|
744 | finally: |
---|
745 | infile.close() |
---|
746 | |
---|
747 | if options.statistics: |
---|
748 | translated = 0 |
---|
749 | for message in list(catalog)[1:]: |
---|
750 | if message.string: |
---|
751 | translated +=1 |
---|
752 | percentage = 0 |
---|
753 | if len(catalog): |
---|
754 | percentage = translated * 100 // len(catalog) |
---|
755 | self.log.info("%d of %d messages (%d%%) translated in %r", |
---|
756 | translated, len(catalog), percentage, po_file) |
---|
757 | |
---|
758 | if catalog.fuzzy and not options.use_fuzzy: |
---|
759 | self.log.warn('catalog %r is marked as fuzzy, skipping', |
---|
760 | po_file) |
---|
761 | continue |
---|
762 | |
---|
763 | for message, errors in catalog.check(): |
---|
764 | for error in errors: |
---|
765 | self.log.error('error: %s:%d: %s', po_file, message.lineno, |
---|
766 | error) |
---|
767 | |
---|
768 | self.log.info('compiling catalog %r to %r', po_file, mo_file) |
---|
769 | |
---|
770 | outfile = open(mo_file, 'wb') |
---|
771 | try: |
---|
772 | write_mo(outfile, catalog, use_fuzzy=options.use_fuzzy) |
---|
773 | finally: |
---|
774 | outfile.close() |
---|
775 | |
---|
776 | def extract(self, argv): |
---|
777 | """Subcommand for extracting messages from source files and generating |
---|
778 | a POT file. |
---|
779 | |
---|
780 | :param argv: the command arguments |
---|
781 | """ |
---|
782 | parser = OptionParser(usage=self.usage % ('extract', 'dir1 <dir2> ...'), |
---|
783 | description=self.commands['extract']) |
---|
784 | parser.add_option('--charset', dest='charset', |
---|
785 | help='charset to use in the output (default ' |
---|
786 | '"%default")') |
---|
787 | parser.add_option('-k', '--keyword', dest='keywords', action='append', |
---|
788 | help='keywords to look for in addition to the ' |
---|
789 | 'defaults. You can specify multiple -k flags on ' |
---|
790 | 'the command line.') |
---|
791 | parser.add_option('--no-default-keywords', dest='no_default_keywords', |
---|
792 | action='store_true', |
---|
793 | help="do not include the default keywords") |
---|
794 | parser.add_option('--mapping', '-F', dest='mapping_file', |
---|
795 | help='path to the extraction mapping file') |
---|
796 | parser.add_option('--no-location', dest='no_location', |
---|
797 | action='store_true', |
---|
798 | help='do not include location comments with filename ' |
---|
799 | 'and line number') |
---|
800 | parser.add_option('--omit-header', dest='omit_header', |
---|
801 | action='store_true', |
---|
802 | help='do not include msgid "" entry in header') |
---|
803 | parser.add_option('-o', '--output', dest='output', |
---|
804 | help='path to the output POT file') |
---|
805 | parser.add_option('-w', '--width', dest='width', type='int', |
---|
806 | help="set output line width (default %default)") |
---|
807 | parser.add_option('--no-wrap', dest='no_wrap', action = 'store_true', |
---|
808 | help='do not break long message lines, longer than ' |
---|
809 | 'the output line width, into several lines') |
---|
810 | parser.add_option('--sort-output', dest='sort_output', |
---|
811 | action='store_true', |
---|
812 | help='generate sorted output (default False)') |
---|
813 | parser.add_option('--sort-by-file', dest='sort_by_file', |
---|
814 | action='store_true', |
---|
815 | help='sort output by file location (default False)') |
---|
816 | parser.add_option('--msgid-bugs-address', dest='msgid_bugs_address', |
---|
817 | metavar='EMAIL@ADDRESS', |
---|
818 | help='set report address for msgid') |
---|
819 | parser.add_option('--copyright-holder', dest='copyright_holder', |
---|
820 | help='set copyright holder in output') |
---|
821 | parser.add_option('--add-comments', '-c', dest='comment_tags', |
---|
822 | metavar='TAG', action='append', |
---|
823 | help='place comment block with TAG (or those ' |
---|
824 | 'preceding keyword lines) in output file. One ' |
---|
825 | 'TAG per argument call') |
---|
826 | parser.add_option('--strip-comment-tags', '-s', |
---|
827 | dest='strip_comment_tags', action='store_true', |
---|
828 | help='Strip the comment tags from the comments.') |
---|
829 | |
---|
830 | parser.set_defaults(charset='utf-8', keywords=[], |
---|
831 | no_default_keywords=False, no_location=False, |
---|
832 | omit_header = False, width=76, no_wrap=False, |
---|
833 | sort_output=False, sort_by_file=False, |
---|
834 | comment_tags=[], strip_comment_tags=False) |
---|
835 | options, args = parser.parse_args(argv) |
---|
836 | if not args: |
---|
837 | parser.error('incorrect number of arguments') |
---|
838 | |
---|
839 | if options.output not in (None, '-'): |
---|
840 | outfile = open(options.output, 'w') |
---|
841 | else: |
---|
842 | outfile = sys.stdout |
---|
843 | |
---|
844 | keywords = DEFAULT_KEYWORDS.copy() |
---|
845 | if options.no_default_keywords: |
---|
846 | if not options.keywords: |
---|
847 | parser.error('you must specify new keywords if you disable the ' |
---|
848 | 'default ones') |
---|
849 | keywords = {} |
---|
850 | if options.keywords: |
---|
851 | keywords.update(parse_keywords(options.keywords)) |
---|
852 | |
---|
853 | if options.mapping_file: |
---|
854 | fileobj = open(options.mapping_file, 'U') |
---|
855 | try: |
---|
856 | method_map, options_map = parse_mapping(fileobj) |
---|
857 | finally: |
---|
858 | fileobj.close() |
---|
859 | else: |
---|
860 | method_map = DEFAULT_MAPPING |
---|
861 | options_map = {} |
---|
862 | |
---|
863 | if options.width and options.no_wrap: |
---|
864 | parser.error("'--no-wrap' and '--width' are mutually exclusive.") |
---|
865 | elif not options.width and not options.no_wrap: |
---|
866 | options.width = 76 |
---|
867 | elif not options.width and options.no_wrap: |
---|
868 | options.width = 0 |
---|
869 | |
---|
870 | if options.sort_output and options.sort_by_file: |
---|
871 | parser.error("'--sort-output' and '--sort-by-file' are mutually " |
---|
872 | "exclusive") |
---|
873 | |
---|
874 | try: |
---|
875 | catalog = Catalog(msgid_bugs_address=options.msgid_bugs_address, |
---|
876 | copyright_holder=options.copyright_holder, |
---|
877 | charset=options.charset) |
---|
878 | |
---|
879 | for dirname in args: |
---|
880 | if not os.path.isdir(dirname): |
---|
881 | parser.error('%r is not a directory' % dirname) |
---|
882 | |
---|
883 | def callback(filename, method, options): |
---|
884 | if method == 'ignore': |
---|
885 | return |
---|
886 | filepath = os.path.normpath(os.path.join(dirname, filename)) |
---|
887 | optstr = '' |
---|
888 | if options: |
---|
889 | optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for |
---|
890 | k, v in options.items()]) |
---|
891 | self.log.info('extracting messages from %s%s', filepath, |
---|
892 | optstr) |
---|
893 | |
---|
894 | extracted = extract_from_dir(dirname, method_map, options_map, |
---|
895 | keywords, options.comment_tags, |
---|
896 | callback=callback, |
---|
897 | strip_comment_tags= |
---|
898 | options.strip_comment_tags) |
---|
899 | for filename, lineno, message, comments in extracted: |
---|
900 | filepath = os.path.normpath(os.path.join(dirname, filename)) |
---|
901 | catalog.add(message, None, [(filepath, lineno)], |
---|
902 | auto_comments=comments) |
---|
903 | |
---|
904 | if options.output not in (None, '-'): |
---|
905 | self.log.info('writing PO template file to %s' % options.output) |
---|
906 | write_po(outfile, catalog, width=options.width, |
---|
907 | no_location=options.no_location, |
---|
908 | omit_header=options.omit_header, |
---|
909 | sort_output=options.sort_output, |
---|
910 | sort_by_file=options.sort_by_file) |
---|
911 | finally: |
---|
912 | if options.output: |
---|
913 | outfile.close() |
---|
914 | |
---|
915 | def init(self, argv): |
---|
916 | """Subcommand for creating new message catalogs from a template. |
---|
917 | |
---|
918 | :param argv: the command arguments |
---|
919 | """ |
---|
920 | parser = OptionParser(usage=self.usage % ('init', ''), |
---|
921 | description=self.commands['init']) |
---|
922 | parser.add_option('--domain', '-D', dest='domain', |
---|
923 | help="domain of PO file (default '%default')") |
---|
924 | parser.add_option('--input-file', '-i', dest='input_file', |
---|
925 | metavar='FILE', help='name of the input file') |
---|
926 | parser.add_option('--output-dir', '-d', dest='output_dir', |
---|
927 | metavar='DIR', help='path to output directory') |
---|
928 | parser.add_option('--output-file', '-o', dest='output_file', |
---|
929 | metavar='FILE', |
---|
930 | help="name of the output file (default " |
---|
931 | "'<output_dir>/<locale>/LC_MESSAGES/" |
---|
932 | "<domain>.po')") |
---|
933 | parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', |
---|
934 | help='locale for the new localized catalog') |
---|
935 | |
---|
936 | parser.set_defaults(domain='messages') |
---|
937 | options, args = parser.parse_args(argv) |
---|
938 | |
---|
939 | if not options.locale: |
---|
940 | parser.error('you must provide a locale for the new catalog') |
---|
941 | try: |
---|
942 | locale = Locale.parse(options.locale) |
---|
943 | except UnknownLocaleError, e: |
---|
944 | parser.error(e) |
---|
945 | |
---|
946 | if not options.input_file: |
---|
947 | parser.error('you must specify the input file') |
---|
948 | |
---|
949 | if not options.output_file and not options.output_dir: |
---|
950 | parser.error('you must specify the output file or directory') |
---|
951 | |
---|
952 | if not options.output_file: |
---|
953 | options.output_file = os.path.join(options.output_dir, |
---|
954 | options.locale, 'LC_MESSAGES', |
---|
955 | options.domain + '.po') |
---|
956 | if not os.path.exists(os.path.dirname(options.output_file)): |
---|
957 | os.makedirs(os.path.dirname(options.output_file)) |
---|
958 | |
---|
959 | infile = open(options.input_file, 'r') |
---|
960 | try: |
---|
961 | # Although reading from the catalog template, read_po must be fed |
---|
962 | # the locale in order to correcly calculate plurals |
---|
963 | catalog = read_po(infile, locale=options.locale) |
---|
964 | finally: |
---|
965 | infile.close() |
---|
966 | |
---|
967 | catalog.locale = locale |
---|
968 | catalog.revision_date = datetime.now(LOCALTZ) |
---|
969 | |
---|
970 | self.log.info('creating catalog %r based on %r', options.output_file, |
---|
971 | options.input_file) |
---|
972 | |
---|
973 | outfile = open(options.output_file, 'w') |
---|
974 | try: |
---|
975 | write_po(outfile, catalog) |
---|
976 | finally: |
---|
977 | outfile.close() |
---|
978 | |
---|
979 | def update(self, argv): |
---|
980 | """Subcommand for updating existing message catalogs from a template. |
---|
981 | |
---|
982 | :param argv: the command arguments |
---|
983 | :since: version 0.9 |
---|
984 | """ |
---|
985 | parser = OptionParser(usage=self.usage % ('update', ''), |
---|
986 | description=self.commands['update']) |
---|
987 | parser.add_option('--domain', '-D', dest='domain', |
---|
988 | help="domain of PO file (default '%default')") |
---|
989 | parser.add_option('--input-file', '-i', dest='input_file', |
---|
990 | metavar='FILE', help='name of the input file') |
---|
991 | parser.add_option('--output-dir', '-d', dest='output_dir', |
---|
992 | metavar='DIR', help='path to output directory') |
---|
993 | parser.add_option('--output-file', '-o', dest='output_file', |
---|
994 | metavar='FILE', |
---|
995 | help="name of the output file (default " |
---|
996 | "'<output_dir>/<locale>/LC_MESSAGES/" |
---|
997 | "<domain>.po')") |
---|
998 | parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', |
---|
999 | help='locale of the translations catalog') |
---|
1000 | parser.add_option('--ignore-obsolete', dest='ignore_obsolete', |
---|
1001 | action='store_true', |
---|
1002 | help='do not include obsolete messages in the output ' |
---|
1003 | '(default %default)'), |
---|
1004 | parser.add_option('--no-fuzzy-matching', '-N', dest='no_fuzzy_matching', |
---|
1005 | action='store_true', |
---|
1006 | help='do not use fuzzy matching (default %default)'), |
---|
1007 | parser.add_option('--previous', dest='previous', action='store_true', |
---|
1008 | help='keep previous msgids of translated messages ' |
---|
1009 | '(default %default)'), |
---|
1010 | |
---|
1011 | parser.set_defaults(domain='messages', ignore_obsolete=False, |
---|
1012 | no_fuzzy_matching=False, previous=False) |
---|
1013 | options, args = parser.parse_args(argv) |
---|
1014 | |
---|
1015 | if not options.input_file: |
---|
1016 | parser.error('you must specify the input file') |
---|
1017 | if not options.output_file and not options.output_dir: |
---|
1018 | parser.error('you must specify the output file or directory') |
---|
1019 | if options.output_file and not options.locale: |
---|
1020 | parser.error('you must specify the loicale') |
---|
1021 | if options.no_fuzzy_matching and options.previous: |
---|
1022 | options.previous = False |
---|
1023 | |
---|
1024 | po_files = [] |
---|
1025 | if not options.output_file: |
---|
1026 | if options.locale: |
---|
1027 | po_files.append((options.locale, |
---|
1028 | os.path.join(options.output_dir, |
---|
1029 | options.locale, 'LC_MESSAGES', |
---|
1030 | options.domain + '.po'))) |
---|
1031 | else: |
---|
1032 | for locale in os.listdir(options.output_dir): |
---|
1033 | po_file = os.path.join(options.output_dir, locale, |
---|
1034 | 'LC_MESSAGES', |
---|
1035 | options.domain + '.po') |
---|
1036 | if os.path.exists(po_file): |
---|
1037 | po_files.append((locale, po_file)) |
---|
1038 | else: |
---|
1039 | po_files.append((options.locale, options.output_file)) |
---|
1040 | |
---|
1041 | domain = options.domain |
---|
1042 | if not domain: |
---|
1043 | domain = os.path.splitext(os.path.basename(options.input_file))[0] |
---|
1044 | |
---|
1045 | infile = open(options.input_file, 'U') |
---|
1046 | try: |
---|
1047 | template = read_po(infile) |
---|
1048 | finally: |
---|
1049 | infile.close() |
---|
1050 | |
---|
1051 | if not po_files: |
---|
1052 | parser.error('no message catalogs found') |
---|
1053 | |
---|
1054 | for locale, filename in po_files: |
---|
1055 | self.log.info('updating catalog %r based on %r', filename, |
---|
1056 | options.input_file) |
---|
1057 | infile = open(filename, 'U') |
---|
1058 | try: |
---|
1059 | catalog = read_po(infile, locale=locale, domain=domain) |
---|
1060 | finally: |
---|
1061 | infile.close() |
---|
1062 | |
---|
1063 | catalog.update(template, options.no_fuzzy_matching) |
---|
1064 | |
---|
1065 | tmpname = os.path.join(os.path.dirname(filename), |
---|
1066 | tempfile.gettempprefix() + |
---|
1067 | os.path.basename(filename)) |
---|
1068 | tmpfile = open(tmpname, 'w') |
---|
1069 | try: |
---|
1070 | try: |
---|
1071 | write_po(tmpfile, catalog, |
---|
1072 | ignore_obsolete=options.ignore_obsolete, |
---|
1073 | include_previous=options.previous) |
---|
1074 | finally: |
---|
1075 | tmpfile.close() |
---|
1076 | except: |
---|
1077 | os.remove(tmpname) |
---|
1078 | raise |
---|
1079 | |
---|
1080 | try: |
---|
1081 | os.rename(tmpname, filename) |
---|
1082 | except OSError: |
---|
1083 | # We're probably on Windows, which doesn't support atomic |
---|
1084 | # renames, at least not through Python |
---|
1085 | # If the error is in fact due to a permissions problem, that |
---|
1086 | # same error is going to be raised from one of the following |
---|
1087 | # operations |
---|
1088 | os.remove(filename) |
---|
1089 | shutil.copy(tmpname, filename) |
---|
1090 | os.remove(tmpname) |
---|
1091 | |
---|
1092 | |
---|
1093 | def main(): |
---|
1094 | return CommandLineInterface().run(sys.argv) |
---|
1095 | |
---|
1096 | def parse_mapping(fileobj, filename=None): |
---|
1097 | """Parse an extraction method mapping from a file-like object. |
---|
1098 | |
---|
1099 | >>> buf = StringIO(''' |
---|
1100 | ... [extractors] |
---|
1101 | ... custom = mypackage.module:myfunc |
---|
1102 | ... |
---|
1103 | ... # Python source files |
---|
1104 | ... [python: **.py] |
---|
1105 | ... |
---|
1106 | ... # Genshi templates |
---|
1107 | ... [genshi: **/templates/**.html] |
---|
1108 | ... include_attrs = |
---|
1109 | ... [genshi: **/templates/**.txt] |
---|
1110 | ... template_class = genshi.template:TextTemplate |
---|
1111 | ... encoding = latin-1 |
---|
1112 | ... |
---|
1113 | ... # Some custom extractor |
---|
1114 | ... [custom: **/custom/*.*] |
---|
1115 | ... ''') |
---|
1116 | |
---|
1117 | >>> method_map, options_map = parse_mapping(buf) |
---|
1118 | >>> len(method_map) |
---|
1119 | 4 |
---|
1120 | |
---|
1121 | >>> method_map[0] |
---|
1122 | ('**.py', 'python') |
---|
1123 | >>> options_map['**.py'] |
---|
1124 | {} |
---|
1125 | >>> method_map[1] |
---|
1126 | ('**/templates/**.html', 'genshi') |
---|
1127 | >>> options_map['**/templates/**.html']['include_attrs'] |
---|
1128 | '' |
---|
1129 | >>> method_map[2] |
---|
1130 | ('**/templates/**.txt', 'genshi') |
---|
1131 | >>> options_map['**/templates/**.txt']['template_class'] |
---|
1132 | 'genshi.template:TextTemplate' |
---|
1133 | >>> options_map['**/templates/**.txt']['encoding'] |
---|
1134 | 'latin-1' |
---|
1135 | |
---|
1136 | >>> method_map[3] |
---|
1137 | ('**/custom/*.*', 'mypackage.module:myfunc') |
---|
1138 | >>> options_map['**/custom/*.*'] |
---|
1139 | {} |
---|
1140 | |
---|
1141 | :param fileobj: a readable file-like object containing the configuration |
---|
1142 | text to parse |
---|
1143 | :return: a `(method_map, options_map)` tuple |
---|
1144 | :rtype: `tuple` |
---|
1145 | :see: `extract_from_directory` |
---|
1146 | """ |
---|
1147 | extractors = {} |
---|
1148 | method_map = [] |
---|
1149 | options_map = {} |
---|
1150 | |
---|
1151 | parser = RawConfigParser() |
---|
1152 | parser._sections = odict(parser._sections) # We need ordered sections |
---|
1153 | parser.readfp(fileobj, filename) |
---|
1154 | for section in parser.sections(): |
---|
1155 | if section == 'extractors': |
---|
1156 | extractors = dict(parser.items(section)) |
---|
1157 | else: |
---|
1158 | method, pattern = [part.strip() for part in section.split(':', 1)] |
---|
1159 | method_map.append((pattern, method)) |
---|
1160 | options_map[pattern] = dict(parser.items(section)) |
---|
1161 | |
---|
1162 | if extractors: |
---|
1163 | for idx, (pattern, method) in enumerate(method_map): |
---|
1164 | if method in extractors: |
---|
1165 | method = extractors[method] |
---|
1166 | method_map[idx] = (pattern, method) |
---|
1167 | |
---|
1168 | return (method_map, options_map) |
---|
1169 | |
---|
1170 | def parse_keywords(strings=[]): |
---|
1171 | """Parse keywords specifications from the given list of strings. |
---|
1172 | |
---|
1173 | >>> kw = parse_keywords(['_', 'dgettext:2', 'dngettext:2,3']) |
---|
1174 | >>> for keyword, indices in sorted(kw.items()): |
---|
1175 | ... print (keyword, indices) |
---|
1176 | ('_', None) |
---|
1177 | ('dgettext', (2,)) |
---|
1178 | ('dngettext', (2, 3)) |
---|
1179 | """ |
---|
1180 | keywords = {} |
---|
1181 | for string in strings: |
---|
1182 | if ':' in string: |
---|
1183 | funcname, indices = string.split(':') |
---|
1184 | else: |
---|
1185 | funcname, indices = string, None |
---|
1186 | if funcname not in keywords: |
---|
1187 | if indices: |
---|
1188 | indices = tuple([(int(x)) for x in indices.split(',')]) |
---|
1189 | keywords[funcname] = indices |
---|
1190 | return keywords |
---|
1191 | |
---|
1192 | |
---|
1193 | if __name__ == '__main__': |
---|
1194 | main() |
---|