# HG changeset patch # User cmlenz # Date 1183026505 0 # Node ID e927dffc9ab43de6f24cfb5d8d44999b9053feb3 # Parent 5c85c0ec4ef89692d966aa742ef782148f0d009f The frontends now provide ways to update existing translations catalogs from a template. Closes #22. diff --git a/ChangeLog b/ChangeLog --- a/ChangeLog +++ b/ChangeLog @@ -3,6 +3,7 @@ (?, from branches/stable/0.9.x) * Added compilation of message catalogs to MO files. + * Added updating of message catalogs from POT files. Version 0.8.1 diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -176,7 +176,9 @@ elif isinstance(revision_date, datetime) and not revision_date.tzinfo: revision_date = revision_date.replace(tzinfo=LOCALTZ) self.revision_date = revision_date #: Last revision date of the catalog - self.fuzzy = fuzzy #: Catalog Header fuzzy bit(True or False) + self.fuzzy = fuzzy #: Catalog header fuzzy bit (`True` or `False`) + + self.obsolete = odict() #: Dictionary of obsolete messages def _get_header_comment(self): comment = self._header_comment @@ -496,7 +498,7 @@ >>> catalog.add(('salad', 'salads'), (u'Salat', u'Salate'), ... locations=[('util.py', 38)]) - >>> rest = catalog.update(template) + >>> catalog.update(template) >>> len(catalog) 2 @@ -512,15 +514,17 @@ >>> msg2.locations [('util.py', 42)] + Messages that are in the catalog but not in the template are removed + from the main collection, but can still be accessed via the `obsolete` + member: + >>> 'head' in catalog False - >>> rest + >>> catalog.obsolete.values() [] :param template: the reference catalog, usually read from a POT file :param fuzzy_matching: whether to use fuzzy matching of message IDs - :return: a list of `Message` objects that the catalog contained before - the updated, but couldn't be found in the template """ messages = self._messages self._messages = odict() @@ -548,7 +552,7 @@ self[message.id] = message - return messages.values() + self.obsolete = messages def _key_for(self, id): """The key for a message is just the singular ID even for pluralizable diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -37,7 +37,7 @@ from babel.util import odict, LOCALTZ __all__ = ['CommandLineInterface', 'compile_catalog', 'extract_messages', - 'new_catalog', 'check_message_extractors'] + 'init_catalog', 'check_message_extractors'] __docformat__ = 'restructuredtext en' @@ -59,7 +59,7 @@ :see: `setuptools `_ """ - description = 'compile a catalog to a binary MO file' + description = 'compile message catalogs to binary MO files' user_options = [ ('domain=', 'D', "domain of PO file (default 'messages')"), @@ -75,7 +75,7 @@ ('use-fuzzy', 'f', 'also include fuzzy translations'), ] - boolean_options = ['use-fuzzy', 'compile-all'] + boolean_options = ['use-fuzzy'] def initialize_options(self): self.domain = 'messages' @@ -341,25 +341,25 @@ 'parameter must be a dictionary') -class new_catalog(Command): - """New catalog command for use in ``setup.py`` scripts. +class init_catalog(Command): + """New catalog initialization command for use in ``setup.py`` scripts. If correctly installed, this command is available to Setuptools-using setup scripts automatically. For projects using plain old ``distutils``, the command needs to be registered explicitly in ``setup.py``:: - from babel.messages.frontend import new_catalog + from babel.messages.frontend import init_catalog setup( ... - cmdclass = {'new_catalog': new_catalog} + cmdclass = {'init_catalog': init_catalog} ) :see: `Integrating new distutils commands `_ :see: `setuptools `_ """ - description = 'create new catalogs based on a catalog template' + description = 'create a new catalog based on a POT file' user_options = [ ('domain=', 'D', "domain of PO file (default 'messages')"), @@ -421,6 +421,94 @@ outfile.close() +class update_catalog(Command): + """Catalog merging command for use in ``setup.py`` scripts. + + If correctly installed, this command is available to Setuptools-using + setup scripts automatically. For projects using plain old ``distutils``, + the command needs to be registered explicitly in ``setup.py``:: + + from babel.messages.frontend import update_catalog + + setup( + ... + cmdclass = {'update_catalog': update_catalog} + ) + + :see: `Integrating new distutils commands `_ + :see: `setuptools `_ + """ + + description = 'update message catalogs from a POT file' + user_options = [ + ('domain=', 'D', + "domain of PO file (default 'messages')"), + ('input-file=', 'i', + 'name of the input file'), + ('output-dir=', 'd', + 'path to base directory containing the catalogs'), + ('output-file=', 'o', + "name of the output file (default " + "'//LC_MESSAGES/.po')"), + ('locale=', 'l', + 'locale of the catalog to compile'), + ] + + def initialize_options(self): + self.domain = 'messages' + self.input_file = None + self.output_dir = None + self.output_file = None + self.locale = None + + def finalize_options(self): + if not self.input_file: + raise DistutilsOptionError('you must specify the input file') + if not self.output_file and not self.output_dir: + raise DistutilsOptionError('you must specify the output file or ' + 'directory') + + def run(self): + po_files = [] + if not self.output_file: + if self.locale: + po_files.append(os.path.join(self.output_dir, self.locale, + 'LC_MESSAGES', + self.domain + '.po')) + else: + for locale in os.listdir(self.output_dir): + po_file = os.path.join(self.output_dir, locale, + 'LC_MESSAGES', + self.domain + '.po') + if os.path.exists(po_file): + po_files.append(po_file) + else: + po_files.append(self.output_file) + + infile = open(self.input_file, 'U') + try: + template = read_po(infile) + finally: + infile.close() + + for po_file in po_files: + log.info('updating catalog %r based on %r', po_file, + self.input_file) + infile = open(po_file, 'U') + try: + catalog = read_po(infile) + finally: + infile.close() + + rest = catalog.update(template) + + outfile = open(po_file, 'w') + try: + write_po(outfile, catalog) + finally: + outfile.close() + + class CommandLineInterface(object): """Command-line interface. @@ -433,7 +521,8 @@ commands = { 'compile': 'compile message catalogs to MO files', 'extract': 'extract messages from source files and generate a POT file', - 'init': 'create new message catalogs from a template', + 'init': 'create new message catalogs from a POT file', + 'update': 'update existing message catalogs from a POT file' } def run(self, argv=sys.argv): @@ -729,6 +818,75 @@ finally: outfile.close() + def update(self, argv): + """Subcommand for updating existing message catalogs from a template. + + :param argv: the command arguments + """ + parser = OptionParser(usage=self.usage % ('update', ''), + description=self.commands['update']) + parser.add_option('--domain', '-D', dest='domain', + help="domain of PO file (default '%default')") + parser.add_option('--input-file', '-i', dest='input_file', + metavar='FILE', help='name of the input file') + parser.add_option('--output-dir', '-d', dest='output_dir', + metavar='DIR', help='path to output directory') + parser.add_option('--output-file', '-o', dest='output_file', + metavar='FILE', + help="name of the output file (default " + "'//LC_MESSAGES/" + ".po')") + parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', + help='locale of the translations catalog') + + parser.set_defaults(domain='messages') + options, args = parser.parse_args(argv) + + if not options.input_file: + parser.error('you must specify the input file') + + if not options.output_file and not options.output_dir: + parser.error('you must specify the output file or directory') + + po_files = [] + if not options.output_file: + if options.locale: + po_files.append(os.path.join(options.output_dir, options.locale, + 'LC_MESSAGES', + options.domain + '.po')) + else: + for locale in os.listdir(options.output_dir): + po_file = os.path.join(options.output_dir, locale, + 'LC_MESSAGES', + options.domain + '.po') + if os.path.exists(po_file): + po_files.append(po_file) + else: + po_files.append(options.output_file) + + infile = open(options.input_file, 'U') + try: + template = read_po(infile) + finally: + infile.close() + + for po_file in po_files: + print 'updating catalog %r based on %r' % (po_file, + options.input_file) + infile = open(po_file, 'U') + try: + catalog = read_po(infile) + finally: + infile.close() + + rest = catalog.update(template) + + outfile = open(po_file, 'w') + try: + write_po(outfile, catalog) + finally: + outfile.close() + def main(): CommandLineInterface().run(sys.argv) diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py --- a/babel/messages/pofile.py +++ b/babel/messages/pofile.py @@ -338,6 +338,24 @@ text = text.encode(catalog.charset) fileobj.write(text) + def _write_comment(comment, prefix=''): + lines = comment + if width and width > 0: + lines = wrap(comment, width, break_long_words=False) + for line in lines: + _write('#%s %s\n' % (prefix, line.strip())) + + def _write_message(message, prefix=''): + if isinstance(message.id, (list, tuple)): + _write('%smsgid %s\n' % (prefix, _normalize(message.id[0]))) + _write('%smsgid_plural %s\n' % (prefix, _normalize(message.id[1]))) + for i, string in enumerate(message.string): + _write('%smsgstr[%d] %s\n' % (prefix, i, + _normalize(message.string[i]))) + else: + _write('%smsgid %s\n' % (prefix, _normalize(message.id))) + _write('%smsgstr %s\n' % (prefix, _normalize(message.string or ''))) + messages = list(catalog) if sort_output: messages.sort(lambda x,y: cmp(x.id, y.id)) @@ -357,32 +375,23 @@ comment_header = u'\n'.join(lines) + u'\n' _write(comment_header) - if message.user_comments: - for comment in message.user_comments: - for line in wrap(comment, width, break_long_words=False): - _write('# %s\n' % line.strip()) - - if message.auto_comments: - for comment in message.auto_comments: - for line in wrap(comment, width, break_long_words=False): - _write('#. %s\n' % line.strip()) + for comment in message.user_comments: + _write_comment(comment) + for comment in message.auto_comments: + _write_comment(comment, prefix='.') if not no_location: locs = u' '.join([u'%s:%d' % (filename.replace(os.sep, '/'), lineno) for filename, lineno in message.locations]) - if width and width > 0: - locs = wrap(locs, width, break_long_words=False) - for line in locs: - _write('#: %s\n' % line.strip()) + _write_comment(locs, prefix=':') if message.flags: _write('#%s\n' % ', '.join([''] + list(message.flags))) - if isinstance(message.id, (list, tuple)): - _write('msgid %s\n' % _normalize(message.id[0])) - _write('msgid_plural %s\n' % _normalize(message.id[1])) - for i, string in enumerate(message.string): - _write('msgstr[%d] %s\n' % (i, _normalize(message.string[i]))) - else: - _write('msgid %s\n' % _normalize(message.id)) - _write('msgstr %s\n' % _normalize(message.string or '')) + _write_message(message) _write('\n') + + for message in catalog.obsolete.values(): + for comment in message.user_comments: + _write_comment(comment) + _write_message(message, prefix='#~ ') + _write('\n') diff --git a/babel/messages/tests/catalog.py b/babel/messages/tests/catalog.py --- a/babel/messages/tests/catalog.py +++ b/babel/messages/tests/catalog.py @@ -62,8 +62,8 @@ cat.add('bar', 'Bahr') tmpl = catalog.Catalog() tmpl.add('Foo') - rest = cat.update(tmpl) - self.assertEqual(1, len(rest)) + cat.update(tmpl) + self.assertEqual(1, len(cat.obsolete)) assert 'foo' not in cat self.assertEqual('Voh', cat['Foo'].string) @@ -75,8 +75,8 @@ cat.add('bar', 'Bahr') tmpl = catalog.Catalog() tmpl.add('foo') - rest = cat.update(tmpl) - self.assertEqual(1, len(rest)) + cat.update(tmpl) + self.assertEqual(1, len(cat.obsolete)) assert 'fo' not in cat self.assertEqual('Voh', cat['foo'].string) @@ -88,8 +88,8 @@ cat.add('bar', 'Bahr') tmpl = catalog.Catalog() tmpl.add('foo') - rest = cat.update(tmpl, fuzzy_matching=False) - self.assertEqual(2, len(rest)) + cat.update(tmpl, fuzzy_matching=False) + self.assertEqual(2, len(cat.obsolete)) def suite(): diff --git a/babel/messages/tests/data/project/i18n/messages_non_fuzzy.pot b/babel/messages/tests/data/project/i18n/messages_non_fuzzy.pot --- a/babel/messages/tests/data/project/i18n/messages_non_fuzzy.pot +++ b/babel/messages/tests/data/project/i18n/messages_non_fuzzy.pot @@ -4,7 +4,6 @@ # project. # FIRST AUTHOR , 2007. # -#, fuzzy msgid "" msgstr "" "Project-Id-Version: TestProject 0.1\n" diff --git a/babel/messages/tests/frontend.py b/babel/messages/tests/frontend.py --- a/babel/messages/tests/frontend.py +++ b/babel/messages/tests/frontend.py @@ -263,7 +263,7 @@ open(pot_file, 'U').read()) -class NewCatalogTestCase(unittest.TestCase): +class InitCatalogTestCase(unittest.TestCase): def setUp(self): self.olddir = os.getcwd() @@ -276,7 +276,7 @@ version='0.1', packages=['project'] )) - self.cmd = frontend.new_catalog(self.dist) + self.cmd = frontend.init_catalog(self.dist) self.cmd.initialize_options() def tearDown(self): @@ -347,7 +347,9 @@ tzinfo=LOCALTZ, locale='en')}, open(po_file, 'U').read()) -class NewNonFuzzyCatalogTestCase(unittest.TestCase): + +class InitCatalogNonFuzzyTestCase(unittest.TestCase): + # FIXME: what is this test case about? def setUp(self): self.olddir = os.getcwd() @@ -360,7 +362,7 @@ version='0.1', packages=['project'] )) - self.cmd = frontend.new_catalog(self.dist) + self.cmd = frontend.init_catalog(self.dist) self.cmd.initialize_options() def tearDown(self): @@ -420,6 +422,7 @@ tzinfo=LOCALTZ, locale='en')}, open(po_file, 'U').read()) + class CommandLineInterfaceTestCase(unittest.TestCase): def setUp(self): @@ -465,7 +468,8 @@ commands: compile compile message catalogs to mo files extract extract messages from source files and generate a pot file - init create new message catalogs from a template + init create new message catalogs from a pot file + update update existing message catalogs from a pot file """, sys.stdout.getvalue().lower()) def test_extract_with_default_mapping(self): @@ -683,7 +687,8 @@ suite.addTest(doctest.DocTestSuite(frontend)) suite.addTest(unittest.makeSuite(CompileCatalogTestCase)) suite.addTest(unittest.makeSuite(ExtractMessagesTestCase)) - suite.addTest(unittest.makeSuite(NewCatalogTestCase)) + suite.addTest(unittest.makeSuite(InitCatalogTestCase)) + suite.addTest(unittest.makeSuite(InitCatalogNonFuzzyTestCase)) suite.addTest(unittest.makeSuite(CommandLineInterfaceTestCase)) return suite diff --git a/babel/messages/tests/pofile.py b/babel/messages/tests/pofile.py --- a/babel/messages/tests/pofile.py +++ b/babel/messages/tests/pofile.py @@ -16,7 +16,7 @@ from StringIO import StringIO import unittest -from babel.messages.catalog import Catalog +from babel.messages.catalog import Catalog, Message from babel.messages import pofile @@ -150,6 +150,22 @@ msgid "bar" msgstr ""''', buf.getvalue().strip()) + def test_po_with_obsolete_messages(self): + catalog = Catalog() + catalog.add(u'foo', u'Voh', locations=[('main.py', 1)]) + catalog.obsolete['bar'] = Message(u'bar', u'Bahr', + locations=[('utils.py', 3)], + user_comments=['User comment']) + buf = StringIO() + pofile.write_po(buf, catalog, omit_header=True) + self.assertEqual('''#: main.py:1 +msgid "foo" +msgstr "Voh" + +# User comment +#~ msgid "bar" +#~ msgstr "Bahr"''', buf.getvalue().strip()) + def suite(): suite = unittest.TestSuite() diff --git a/doc/cmdline.txt b/doc/cmdline.txt --- a/doc/cmdline.txt +++ b/doc/cmdline.txt @@ -26,7 +26,8 @@ subcommands: compile compile message catalogs to MO files extract extract messages from source files and generate a POT file - init create new message catalogs from a template + init create new message catalogs from a POT file + update update existing message catalogs from a POT file The ``babel`` script provides a number of sub-commands that do the actual work. Those sub-commands are described below. @@ -120,7 +121,7 @@ $ babel init --help usage: babel init [options] - create new message catalogs from a template + create new message catalogs from a POT file options: -h, --help show this help message and exit @@ -143,3 +144,38 @@ --project-name=NAME the project name --project-version=VERSION the project version + + +update +====== + +The `update` sub-command updates an existing new translations catalog based on +a PO template file:: + + $ babel update --help + usage: babel update [options] + + update existing message catalogs from a POT file + + options: + -h, --help show this help message and exit + -D DOMAIN, --domain=DOMAIN + domain of PO file (default 'messages') + -i FILE, --input-file=FILE + name of the input file + -d DIR, --output-dir=DIR + path to output directory + -o FILE, --output-file=FILE + name of the output file (default + '//LC_MESSAGES/.po') + -l LOCALE, --locale=LOCALE + locale of the translations catalog + +If ``output_dir`` is specified, but ``output-file`` is not, the default +filename of the output file will be:: + + //LC_MESSAGES/.mo + +If neither the ``output_file`` nor the ``locale`` option is set, this command +looks for all catalog files in the base directory that match the given domain, +and updates each of them. diff --git a/doc/setup.txt b/doc/setup.txt --- a/doc/setup.txt +++ b/doc/setup.txt @@ -21,7 +21,7 @@ ... cmd_class = {'compile_catalog': babel.compile_catalog, 'extract_messages': babel.extract_messages, - 'new_catalog': babel.new_catalog} + 'init_catalog': babel.init_catalog} ) @@ -214,37 +214,37 @@ file. For boolean options, use "true" or "false" values. -new_catalog -=========== +init_catalog +============ -The ``new_catalog`` command is basically equivalent to the GNU ``msginit`` +The ``init_catalog`` command is basically equivalent to the GNU ``msginit`` program: it creates a new translation catalog based on a PO template file (POT). If the command has been correctly installed or registered, a project's ``setup.py`` script should allow you to use the command:: - $ ./setup.py new_catalog --help + $ ./setup.py init_catalog --help Global options: --verbose (-v) run verbosely (default) --quiet (-q) run quietly (turns verbosity off) --dry-run (-n) don't actually do anything --help (-h) show detailed help message - Options for 'new_catalog' command: + Options for 'init_catalog' command: ... Running the command will produce a PO file:: - $ ./setup.py new_catalog -l fr -i foobar/locales/messages.pot \ + $ ./setup.py init_catalog -l fr -i foobar/locales/messages.pot \ -o foobar/locales/fr/messages.po - running new_catalog + running init_catalog creating catalog 'foobar/locales/fr/messages.po' based on 'foobar/locales/messages.pot' Options ------- -The ``new_catalog`` command accepts the following options: +The ``init_catalog`` command accepts the following options: +-----------------------------+----------------------------------------------+ | Option | Description | @@ -274,3 +274,64 @@ These options can either be specified on the command-line, or in the ``setup.cfg`` file. + + +update_catalog +============== + +The ``update_catalog`` command is basically equivalent to the GNU ``msgmerge`` +program: it updates an existing translations catalog based on a PO template +file (POT). + +If the command has been correctly installed or registered, a project's +``setup.py`` script should allow you to use the command:: + + $ ./setup.py update_catalog --help + Global options: + --verbose (-v) run verbosely (default) + --quiet (-q) run quietly (turns verbosity off) + --dry-run (-n) don't actually do anything + --help (-h) show detailed help message + + Options for 'update_catalog' command: + ... + +Running the command will update a PO file:: + + $ ./setup.py update_catalog -l fr -i foobar/locales/messages.pot \ + -o foobar/locales/fr/messages.po + running update_catalog + updating catalog 'foobar/locales/fr/messages.po' based on 'foobar/locales/messages.pot' + + +Options +------- + +The ``update_catalog`` command accepts the following options: + + +-----------------------------+----------------------------------------------+ + | Option | Description | + +=============================+==============================================+ + | ``--domain`` | domain of the PO file (defaults to | + | | lower-cased project name) | + +-----------------------------+----------------------------------------------+ + | ``--input-file`` (``-i``) | name of the input file | + +-----------------------------+----------------------------------------------+ + | ``--output-dir`` (``-d``) | name of the output directory | + +-----------------------------+----------------------------------------------+ + | ``--output-file`` (``-o``) | name of the output file | + +-----------------------------+----------------------------------------------+ + | ``--locale`` | locale for the new localized string | + +-----------------------------+----------------------------------------------+ + +If ``output-dir`` is specified, but ``output-file`` is not, the default filename +of the output file will be:: + + //LC_MESSAGES/.po + +If neither the ``input_file`` nor the ``locale`` option is set, this command +looks for all catalog files in the base directory that match the given domain, +and updates each of them. + +These options can either be specified on the command-line, or in the +``setup.cfg`` file. diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -140,7 +140,8 @@ [distutils.commands] compile_catalog = babel.messages.frontend:compile_catalog extract_messages = babel.messages.frontend:extract_messages - new_catalog = babel.messages.frontend:new_catalog + init_catalog = babel.messages.frontend:init_catalog + update_catalog = babel.messages.frontend:update_catalog [distutils.setup_keywords] message_extractors = babel.messages.frontend:check_message_extractors