# HG changeset patch # User palgarvio # Date 1181256527 0 # Node ID 7f61453c1beabcf270b94bbdaddaabce1a885405 # Parent fa8a27b80eb4a8a3fc391cfd11da6b2bb1a995b0 Fixed a bug regarding plural msgid's handling when writing the `.pot` file. Renamed old `write_po` to `write_pot` which is what it actually does and also adds space to the new `write_po`. Changed tests accordingly. Added support to create new localized catalogs from a catalog template, `write_po`.. diff --git a/babel/catalog/frontend.py b/babel/catalog/frontend.py --- a/babel/catalog/frontend.py +++ b/babel/catalog/frontend.py @@ -24,9 +24,12 @@ import sys from babel import __version__ as VERSION +from babel import Locale +from babel.core import UnknownLocaleError from babel.catalog.extract import extract_from_dir, DEFAULT_KEYWORDS, \ DEFAULT_MAPPING -from babel.catalog.pofile import write_po +from babel.catalog.pofile import write_po, write_pot +from babel.catalog.plurals import PLURALS __all__ = ['extract_messages', 'check_message_extractors', 'main'] __docformat__ = 'restructuredtext en' @@ -147,31 +150,15 @@ filepath = os.path.normpath(filename) messages.append((filepath, lineno, funcname, message, None)) - log.info('writing PO file to %s' % self.output_file) - write_po(outfile, messages, project=self.distribution.get_name(), + log.info('writing PO template file to %s' % self.output_file) + write_pot(outfile, messages, project=self.distribution.get_name(), version=self.distribution.get_version(), width=self.width, charset=self.charset, no_location=self.no_location, omit_header=self.omit_header) finally: outfile.close() -def check_message_extractors(dist, name, value): - """Validate the ``message_extractors`` keyword argument to ``setup()``. - - :param dist: the distutils/setuptools ``Distribution`` object - :param name: the name of the keyword argument (should always be - "message_extractors") - :param value: the value of the keyword argument - :raise `DistutilsSetupError`: if the value is not valid - :see: `Adding setup() arguments - `_ - """ - assert name == 'message_extractors' - if not isinstance(value, (basestring, dict)): - raise DistutilsSetupError('the value of the "extract_messages" ' - 'parameter must be a string or dictionary') - -def main(argv=sys.argv): +def extract_cmdline(argv=sys.argv): """Command-line interface. This function provides a simple command-line interface to the message @@ -259,6 +246,135 @@ finally: if options.output: outfile.close() + +class new_catalog(Command): + """New catalog 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.catalog.frontend import new_catalog + + setup( + ... + cmdclass = {'new_catalog': new_catalog} + ) + + :see: `Integrating new distutils commands `_ + :see: `setuptools `_ + """ + + description = 'create new catalogs based on a catalog template' + user_options = [ + ('input-file=', 'i', + 'name of the input file'), + ('output-dir=', 'd', + 'path to output directory'), + ('output-file=', 'o', + "name of the output file (default " + "'/.po')"), + ('locale=', 'l', + 'locale for the new localized catalog'), + ('first-author=', None, + 'name of first author'), + ('first-author-email=', None, + 'email of first author') + ] + + def initialize_options(self): + self.output_dir = None + self.output_file = None + self.input_file = None + self.locale = None + self.first_author = None + self.first_author_email = None + + def finalize_options(self): + if not self.input_file: + raise DistutilsOptionError('you must specify the input file') + + if not self.locale: + raise DistutilsOptionError('you must provide a locale for the ' + 'new catalog') + else: + try: + locale = Locale.parse(self.locale) + except UnknownLocaleError, error: + log.error(error) + sys.exit(1) + + self._locale_parts = self.locale.split('_') + self._language = None + self._country = None + _locale = Locale('en') + if len(self._locale_parts) == 2: + if self._locale_parts[0] == self._locale_parts[1].lower(): + # Remove country part if equal to language + locale = self._locale_parts[0] + else: + locale = self.locale + self._language = _locale.languages[self._locale_parts[0]] + self._country = _locale.territories[self._locale_parts[1]] + else: + locale = self._locale_parts[0] + self._language = _locale.languages[locale] + + if not self.output_file and not self.output_dir: + raise DistutilsOptionError('you must specify the output directory') + + if not self.output_file and self.output_dir: + self.output_file = os.path.join(self.output_dir, locale + '.po') + + + def run(self): + outfile = open(self.output_file, 'w') + infile = open(self.input_file, 'r') + + if PLURALS.has_key(self.locale): + # Try _ + plurals = PLURALS[self.locale] + elif PLURALS.has_key(self._locale_parts[0]): + # Try + plurals = PLURALS[self._locale_parts[0]] + else: + plurals = ('INTEGER', 'EXPRESSION') + + if self._country: + logline = 'Creating %%s (%s) %%r PO from %%r' % self._country + \ + ' PO template' + else: + logline = 'Creating %s %r PO from %r PO template' + log.info(logline, self._language, self.output_file, self.input_file) + + write_po(outfile, infile, self._language, country=self._country, + project=self.distribution.get_name(), + version=self.distribution.get_version(), + charset=self.charset, plurals=plurals, + first_author=self.first_author, + first_author_email=self.first_author_email) + infile.close() + outfile.close() + + +def new_catalog_cmdline(argv=sys.argv): + pass + +def check_message_extractors(dist, name, value): + """Validate the ``message_extractors`` keyword argument to ``setup()``. + + :param dist: the distutils/setuptools ``Distribution`` object + :param name: the name of the keyword argument (should always be + "message_extractors") + :param value: the value of the keyword argument + :raise `DistutilsSetupError`: if the value is not valid + :see: `Adding setup() arguments + `_ + """ + assert name == 'message_extractors' + if not isinstance(value, (basestring, dict)): + raise DistutilsSetupError('the value of the "extract_messages" ' + 'parameter must be a string or dictionary') def parse_mapping(fileobj, filename=None): """Parse an extraction method mapping from a file-like object. @@ -333,4 +449,4 @@ return keywords if __name__ == '__main__': - main() + extract_cmdline() diff --git a/babel/catalog/plurals.py b/babel/catalog/plurals.py new file mode 100644 --- /dev/null +++ b/babel/catalog/plurals.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://babel.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://babel.edgewall.org/log/. + +PLURALS = { + # Afrikaans - From Pootle's PO's + 'af': (2, '(n != 1)'), + # Arabic - From Pootle's PO's + 'ar': (6, '(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n>=3 && n<=10 ? 3 : n>=11 && n<=99 ? 4 : 5)'), + # Bulgarian - From Pootle's PO's + 'bg': (2, '(n != 1)'), + # Bengali - From Pootle's PO's + 'bn': (2, '(n != 1)'), + # Catalan - From Pootle's PO's + 'ca': (2, '(n != 1)'), + # Czech + 'cs': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Danish + 'da': (2, '(n != 1)'), + # German + 'de': (2, '(n != 1)'), + # Greek + 'el': (2, '(n != 1)'), + # English + 'en': (2, '(n != 1)'), + # Esperanto + 'eo': (2, '(n != 1)'), + # Spanish + 'es': (2, '(n != 1)'), + # Estonian + 'et': (2, '(n != 1)'), + # Basque - From Pootle's PO's + 'eu': (2, '(n != 1)'), + # Persian - From Pootle's PO's + 'fa': (1, '0'), + # Finnish + 'fi': (2, '(n != 1)'), + # French + 'fr': (2, '(n > 1)'), + # Furlan - From Pootle's PO's + 'fur': (2, '(n > 1)'), + # Irish + 'ga': (3, 'n==1 ? 0 : n==2 ? 1 : 2'), + # Galego - From Pootle's PO's + 'gl': (2, '(n != 1)'), + # Hausa - From Pootle's PO's + 'ha': (2, '(n != 1)'), + # Hebrew + 'he': (2, '(n != 1)'), + # Hindi - From Pootle's PO's + 'hi': (2, '(n != 1)'), + # Croatian + 'hr': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Hungarian + 'hu': (1, '0'), + # Armenian - From Pootle's PO's + 'hy': (1, '0'), + # Icelandic - From Pootle's PO's + 'is': (2, '(n != 1)'), + # Italian + 'it': (2, '(n != 1)'), + # Japanese + 'ja': (1, '0'), + # Georgian - From Pootle's PO's + 'ka': (1, '0'), + # Kongo - From Pootle's PO's + 'kg': (2, '(n != 1)'), + # Khmer - From Pootle's PO's + 'km': (1, '0'), + # Korean + 'ko': (1, '0'), + # KurdĂ® - From Pootle's PO's + 'ku': (2, '(n != 1)'), + # Lithuanian + 'lt': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Latvian + 'lv': (3, '(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2)'), + # Maltese - From Pootle's PO's + 'mt': (4, '(n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3)'), + # Norwegian Bokmal + 'nb': (2, '(n != 1)'), + # Dutch + 'nl': (2, '(n != 1)'), + # Norwegian Nynorsk + 'nn': (2, '(n != 1)'), + # Norwegian + 'no': (2, '(n != 1)'), + # Punjabi - From Pootle's PO's + 'pa': (2, '(n != 1)'), + # Polish + 'pl': (3, '(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Portuguese + 'pt': (2, '(n != 1)'), + # Brazilian + 'pt_BR': (2, '(n > 1)'), + # Romanian - From Pootle's PO's + 'ro': (3, '(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2)'), + # Russian + 'ru': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Slovak + 'sk': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Slovenian + 'sl': (4, '(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3)'), + # Serbian - From Pootle's PO's + 'sr': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10< =4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Sesotho - From Pootle's PO's + 'st': (2, '(n != 1)'), + # Swedish + 'sv': (2, '(n != 1)'), + # Turkish + 'tr': (1, '0'), + # Ukrainian + 'uk': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'), + # Venda - From Pootle's PO's + 've': (2, '(n != 1)'), + # Vietnamese - From Pootle's PO's + 'vi': (1, '0'), + # Xhosa - From Pootle's PO's + 'xh': (2, '(n != 1)'), + # Chinese - From Pootle's PO's + 'zh_CN': (1, '0'), + 'zh_HK': (1, '0'), + 'zh_TW': (1, '0'), +} diff --git a/babel/catalog/pofile.py b/babel/catalog/pofile.py --- a/babel/catalog/pofile.py +++ b/babel/catalog/pofile.py @@ -29,7 +29,7 @@ from babel import __version__ as VERSION -__all__ = ['escape', 'normalize', 'read_po', 'write_po'] +__all__ = ['escape', 'normalize', 'read_po', 'write_po', 'write_pot'] def read_po(fileobj): """Read messages from a ``gettext`` PO (portable object) file from the given @@ -238,13 +238,12 @@ if not lines[-1]: del lines[-1] lines[-1] += '\n' - return u'""\n' + u'\n'.join([escape(l) for l in lines]) -def write_po(fileobj, messages, project='PROJECT', version='VERSION', width=76, +def write_pot(fileobj, messages, project='PROJECT', version='VERSION', width=76, charset='utf-8', no_location=False, omit_header=False): - r"""Write a ``gettext`` PO (portable object) file to the given file-like - object. + r"""Write a ``gettext`` PO (portable object) template file to the given + file-like object. The `messages` parameter is expected to be an iterable object producing tuples of the form: @@ -253,7 +252,7 @@ >>> from StringIO import StringIO >>> buf = StringIO() - >>> write_po(buf, [ + >>> write_pot(buf, [ ... ('main.py', 1, None, u'foo %(name)s', ('fuzzy',)), ... ('main.py', 3, 'ngettext', (u'bar', u'baz'), None) ... ], omit_header=True) @@ -303,19 +302,23 @@ locations = {} msgflags = {} msgids = [] + plurals = {} for filename, lineno, funcname, key, flags in messages: flags = set(flags or []) + if isinstance(key, (list, tuple)): + assert len(key) == 2 + plurals[key[0]] = key[1] + key = key[0] if key in msgids: locations[key].append((filename, lineno)) msgflags[key] |= flags else: - if (isinstance(key, (list, tuple)) and - filter(None, [PYTHON_FORMAT(k) for k in key])) or \ - (isinstance(key, basestring) and PYTHON_FORMAT(key)): + if PYTHON_FORMAT(key): flags.add('python-format') else: - flags.discard('python-format') + flags.discard('python-format') + locations[key] = [(filename, lineno)] msgflags[key] = flags msgids.append(key) @@ -331,13 +334,139 @@ if flags: _write('#%s\n' % ', '.join([''] + list(flags))) - if type(msgid) is tuple: - assert len(msgid) == 2 - _write('msgid %s\n' % _normalize(msgid[0])) - _write('msgid_plural %s\n' % _normalize(msgid[1])) + if plurals.has_key(msgid): + _write('msgid %s\n' % _normalize(msgid)) + _write('msgid_plural %s\n' % _normalize(plurals[msgid])) _write('msgstr[0] ""\n') _write('msgstr[1] ""\n') else: _write('msgid %s\n' % _normalize(msgid)) _write('msgstr ""\n') _write('\n') + +def write_po(fileobj, input_fileobj, language, country=None, project='PROJECT', + version='VERSION', first_author=None, first_author_email=None, + plurals=('INTEGER', 'EXPRESSION')): + r"""Write a ``gettext`` PO (portable object) file to the given file-like + object, from the given input PO template file. + + >>> from StringIO import StringIO + >>> inbuf = StringIO(r'''# Translations Template for FooBar. + ... # Copyright (C) 2007 ORGANIZATION + ... # This file is distributed under the same license as the + ... # FooBar project. + ... # FIRST AUTHOR , YEAR. + ... # + ... #, fuzzy + ... msgid "" + ... msgstr "" + ... "Project-Id-Version: FooBar 0.1\n" + ... "POT-Creation-Date: 2007-06-07 22:54+0100\n" + ... "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" + ... "Last-Translator: FULL NAME \n" + ... "Language-Team: LANGUAGE \n" + ... "MIME-Version: 1.0\n" + ... "Content-Type: text/plain; charset=utf-8\n" + ... "Content-Transfer-Encoding: 8bit\n" + ... "Generated-By: Babel 0.1dev-r50\n" + ... + ... #: base.py:83 templates/index.html:9 + ... #: templates/index2.html:9 + ... msgid "Home" + ... msgstr "" + ... + ... #: base.py:84 templates/index.html:9 + ... msgid "Accounts" + ... msgstr "" + ... ''') + >>> outbuf = StringIO() + >>> write_po(outbuf, inbuf, 'English', project='FooBar', + ... version='0.1', first_author='A Name', + ... first_author_email='user@domain.tld', + ... plurals=(2, '(n != 1)')) + >>> print outbuf.getvalue() # doctest: +ELLIPSIS + # English Translations for FooBar + # Copyright (C) 2007 ORGANIZATION + # This file is distributed under the same license as the + # FooBar project. + # A Name , ... + # + #, fuzzy + msgid "" + msgstr "" + "Project-Id-Version: FooBar 0.1\n" + "POT-Creation-Date: 2007-06-07 22:54+0100\n" + "PO-Revision-Date: ...\n" + "Last-Translator: A Name \n" + "Language-Team: LANGUAGE \n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=utf-8\n" + "Content-Transfer-Encoding: 8bit\n" + "Plural-Forms: nplurals=2; plural=(n != 1);\n" + "Generated-By: Babel ...\n" + + #: base.py:83 templates/index.html:9 + #: templates/index2.html:9 + msgid "Home" + msgstr "" + + #: base.py:84 templates/index.html:9 + msgid "Accounts" + msgstr "" + + >>> + """ + + _first_author = '' + if first_author: + _first_author += first_author + if first_author_email: + _first_author += ' <%s>' % first_author_email + + inlines = input_fileobj.readlines() + outlines = [] + in_header = True + for index in range(len(inlines)): + if in_header: + if '# Translations Template' in inlines[index]: + if country: + line = '# %s (%s) Translations for %%s\n' % \ + (language, country) + else: + line = '# %s Translations for %%s\n' % language + outlines.append(line % project) + elif '# FIRST AUTHOR , YEAR.' in inlines[index]: + if _first_author: + outlines.append( + '# %s, %s\n' % (_first_author, time.strftime('%Y')) + ) + else: + outlines.append(inlines[index]) + elif '"PO-Revision-Date:' in inlines[index]: + outlines.append( + '"PO-Revision-Date: %s\\n"\n' % \ + time.strftime('%Y-%m-%d %H:%M%z') + ) + elif '"Last-Translator:' in inlines[index]: + if _first_author: + outlines.append( + '"Last-Translator: %s\\n"\n' % _first_author + ) + else: + outlines.append(inlines[index]) + elif '"Content-Transfer-Encoding:' in inlines[index]: + outlines.append(inlines[index]) + if '"Plural-Forms:' not in inlines[index+1]: + outlines.append( + '"Plural-Forms: nplurals=%s; plural=%s;\\n"\n' % plurals + ) + elif inlines[index].endswith('\\n"\n') and \ + inlines[index+1] == '\n': + in_header = False + outlines.append(inlines[index]) + else: + outlines.append(inlines[index]) + else: + outlines.extend(inlines[index:]) + break + fileobj.writelines(outlines) diff --git a/babel/catalog/tests/pofile.py b/babel/catalog/tests/pofile.py --- a/babel/catalog/tests/pofile.py +++ b/babel/catalog/tests/pofile.py @@ -26,11 +26,11 @@ assert pofile.PYTHON_FORMAT('foo %r bar') -class WritePoTestCase(unittest.TestCase): +class WritePotTestCase(unittest.TestCase): def test_join_locations(self): buf = StringIO() - pofile.write_po(buf, [ + pofile.write_pot(buf, [ ('main.py', 1, None, u'foo', None), ('utils.py', 3, None, u'foo', None), ], omit_header=True) @@ -46,7 +46,7 @@ """ buf = StringIO() - pofile.write_po(buf, [ + pofile.write_pot(buf, [ ('main.py', 1, None, text, None), ], no_location=True, omit_header=True, width=42) self.assertEqual(r'''msgid "" @@ -63,7 +63,7 @@ includesareallylongwordthatmightbutshouldnt throw us into an infinite loop """ buf = StringIO() - pofile.write_po(buf, [ + pofile.write_pot(buf, [ ('main.py', 1, None, text, None), ], no_location=True, omit_header=True, width=32) self.assertEqual(r'''msgid "" @@ -78,7 +78,7 @@ suite = unittest.TestSuite() suite.addTest(doctest.DocTestSuite(pofile)) suite.addTest(unittest.makeSuite(PythonFormatFlagTestCase)) - suite.addTest(unittest.makeSuite(WritePoTestCase)) + suite.addTest(unittest.makeSuite(WritePotTestCase)) return suite if __name__ == '__main__': diff --git a/doc/setup.txt b/doc/setup.txt --- a/doc/setup.txt +++ b/doc/setup.txt @@ -59,14 +59,14 @@ --no-wrap do not break long message lines, longer than the output line width, into several lines -Running the command will produce a PO file:: +Running the command will produce a PO template file:: $ ./setup.py extract_messages --output-file foobar/locale/messages.pot running extract_messages extracting messages from foobar/__init__.py extracting messages from foobar/core.py ... - writing PO file to foobar/locale/messages.pot + writing PO template file to foobar/locale/messages.pot Method Mapping diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -127,10 +127,12 @@ entry_points = """ [console_scripts] - pygettext = babel.catalog.frontend:main + pygettext = babel.catalog.frontend:extract_cmdline + pymsginit = babel.catalog.frontend:new_catalog_cmdline [distutils.commands] extract_messages = babel.catalog.frontend:extract_messages + new_catalog = babel.catalog.frontend:new_catalog [distutils.setup_keywords] message_extractors = babel.catalog.frontend:check_message_extractors