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`..
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'
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
- <http://peak.telecommunity.com/DevCenter/setuptools#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
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 <http://docs.python.org/dist/node32.html>`_
+ :see: `setuptools <http://peak.telecommunity.com/DevCenter/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 "
+ "'<output_dir>/<locale>.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 <language>_<COUNTRY>
+ plurals = PLURALS[self.locale]
+ elif PLURALS.has_key(self._locale_parts[0]):
+ # Try <language>
+ 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
+ <http://peak.telecommunity.com/DevCenter/setuptools#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.
return keywords
if __name__ == '__main__':
- main()
+ extract_cmdline()
--- /dev/null
+# -*- 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'),
+}
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
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:
>>> 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)
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)
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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
+ ... "Language-Team: LANGUAGE <LL@li.org>\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 <user@domain.tld>, ...
+ #
+ #, 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 <user@domain.tld>\n"
+ "Language-Team: LANGUAGE <LL@li.org>\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"
+ <BLANKLINE>
+ #: base.py:83 templates/index.html:9
+ #: templates/index2.html:9
+ msgid "Home"
+ msgstr ""
+ <BLANKLINE>
+ #: base.py:84 templates/index.html:9
+ msgid "Accounts"
+ msgstr ""
+ <BLANKLINE>
+ >>>
+ """
+
+ _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 <EMAIL@ADDRESS>, 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)
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)
"""
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 ""
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 ""
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__':
--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
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