(?, 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
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
>>> catalog.add(('salad', 'salads'), (u'Salat', u'Salate'),
... locations=[('util.py', 38)])
- >>> rest = catalog.update(template)
+ >>> catalog.update(template)
>>> len(catalog)
2
>>> 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()
[<Message 'head' (Flags: '')>]
: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()
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
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'
:see: `setuptools <http://peak.telecommunity.com/DevCenter/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')"),
('use-fuzzy', 'f',
'also include fuzzy translations'),
]
- boolean_options = ['use-fuzzy', 'compile-all']
+ boolean_options = ['use-fuzzy']
def initialize_options(self):
self.domain = 'messages'
'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 <http://docs.python.org/dist/node32.html>`_
:see: `setuptools <http://peak.telecommunity.com/DevCenter/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')"),
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 <http://docs.python.org/dist/node32.html>`_
+ :see: `setuptools <http://peak.telecommunity.com/DevCenter/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 "
+ "'<output_dir>/<locale>/LC_MESSAGES/<domain>.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.
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):
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 "
+ "'<output_dir>/<locale>/LC_MESSAGES/"
+ "<domain>.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)
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))
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')
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)
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)
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():
# project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
#
-#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: TestProject 0.1\n"
open(pot_file, 'U').read())
-class NewCatalogTestCase(unittest.TestCase):
+class InitCatalogTestCase(unittest.TestCase):
def setUp(self):
self.olddir = os.getcwd()
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):
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()
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):
tzinfo=LOCALTZ, locale='en')},
open(po_file, 'U').read())
+
class CommandLineInterfaceTestCase(unittest.TestCase):
def setUp(self):
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):
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
from StringIO import StringIO
import unittest
-from babel.messages.catalog import Catalog
+from babel.messages.catalog import Catalog, Message
from babel.messages import pofile
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()
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.
$ 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
--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
+ '<output_dir>/<locale>/LC_MESSAGES/<domain>.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::
+
+ <directory>/<locale>/LC_MESSAGES/<domain>.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.
...
cmd_class = {'compile_catalog': babel.compile_catalog,
'extract_messages': babel.extract_messages,
- 'new_catalog': babel.new_catalog}
+ 'init_catalog': babel.init_catalog}
)
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 |
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::
+
+ <output_dir>/<locale>/LC_MESSAGES/<domain>.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.
[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