--- a/cubicweb/devtools/devctl.py Tue Sep 27 12:28:39 2016 +0200
+++ b/cubicweb/devtools/devctl.py Fri Oct 21 18:10:15 2016 +0200
@@ -29,9 +29,11 @@
import sys
from datetime import datetime, date
from os import mkdir, chdir, path as osp
+import pkg_resources
from warnings import warn
from pytz import UTC
+
from six.moves import input
from logilab.common import STD_BLACKLIST
@@ -566,14 +568,21 @@
cubedir = osp.abspath(osp.normpath(cubedir))
workdir = tempfile.mkdtemp()
try:
- cube = osp.basename(cubedir)
+ distname = osp.basename(cubedir)
+ cubename = distname.split('_')[-1]
print('cubedir', cubedir)
- print(underline_title('Updating i18n catalogs for cube %s' % cube))
+ extract_cls = I18nCubeMessageExtractor
+ try:
+ extract_cls = pkg_resources.load_entry_point(
+ distname, 'cubicweb.i18ncube', cubename)
+ except (pkg_resources.DistributionNotFound, ImportError):
+ pass # no customization found
+ print(underline_title('Updating i18n catalogs for cube %s' % cubename))
chdir(cubedir)
- extractor = I18nCubeMessageExtractor(workdir, cubedir)
+ extractor = extract_cls(workdir, cubedir)
potfile = extractor.generate_pot_file()
if potfile is None:
- print('no message catalog for cube', cube, 'nothing to translate')
+ print('no message catalog for cube', cubename, 'nothing to translate')
return ()
print('-> merging main pot file with existing translations:', end=' ')
chdir('i18n')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/devtools/test/data/libpython/cubicweb_i18ntestcube/excludeme/somefile.py Fri Oct 21 18:10:15 2016 +0200
@@ -0,0 +1,4 @@
+from cubicweb import _
+
+_('ignore-me')
+
--- a/cubicweb/devtools/test/data/libpython/cubicweb_i18ntestcube/i18n/en.po.ref Tue Sep 27 12:28:39 2016 +0200
+++ b/cubicweb/devtools/test/data/libpython/cubicweb_i18ntestcube/i18n/en.po.ref Fri Oct 21 18:10:15 2016 +0200
@@ -87,6 +87,9 @@
msgid "description_format"
msgstr ""
+msgid "ignore-me"
+msgstr ""
+
msgid "in_forum"
msgstr ""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/devtools/test/data/libpython/cubicweb_i18ntestcube/node_modules/cubes.somefile.js Fri Oct 21 18:10:15 2016 +0200
@@ -0,0 +1,2 @@
+_("hello");
+
--- a/cubicweb/devtools/test/unittest_i18n.py Tue Sep 27 12:28:39 2016 +0200
+++ b/cubicweb/devtools/test/unittest_i18n.py Fri Oct 21 18:10:15 2016 +0200
@@ -18,13 +18,19 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""unit tests for i18n messages generator"""
+from contextlib import contextmanager
+from io import StringIO, BytesIO
import os
import os.path as osp
import sys
from subprocess import PIPE, Popen, STDOUT
-
from unittest import TestCase, main
+from six import PY2
+from mock import patch
+
+from cubicweb.devtools import devctl
+from cubicweb.devtools.testlib import BaseTestCase
DATADIR = osp.join(osp.abspath(osp.dirname(__file__)), 'data')
@@ -52,6 +58,9 @@
return msgs
+TESTCUBE_DIR = osp.join(DATADIR, 'cubes', 'i18ntestcube')
+
+
class cubePotGeneratorTC(TestCase):
"""test case for i18n pot file generator"""
@@ -87,5 +96,61 @@
self.assertEqual(msgs, newmsgs)
+class CustomMessageExtractor(devctl.I18nCubeMessageExtractor):
+ blacklist = devctl.I18nCubeMessageExtractor.blacklist | set(['excludeme'])
+
+
+@contextmanager
+def capture_stdout():
+ stream = BytesIO() if PY2 else StringIO()
+ sys.stdout = stream
+ yield stream
+ stream.seek(0)
+ sys.stdout = sys.__stdout__
+
+
+class I18nCollectorTest(BaseTestCase):
+
+ def test_i18ncube_py_collection(self):
+ extractor = CustomMessageExtractor(DATADIR, TESTCUBE_DIR)
+ collected = extractor.collect_py()
+ expected = [osp.join(TESTCUBE_DIR, path)
+ for path in ('__init__.py', '__pkginfo__.py',
+ 'views.py', 'schema.py')]
+ self.assertCountEqual(expected, collected)
+
+ def test_i18ncube_js_collection(self):
+ extractor = CustomMessageExtractor(DATADIR, TESTCUBE_DIR)
+ collected = extractor.collect_js()
+ self.assertCountEqual([], collected, [])
+ extractor.blacklist = () # don't ignore anything
+ collected = extractor.collect_js()
+ expected = [osp.join(TESTCUBE_DIR, 'node_modules/cubes.somefile.js')]
+ self.assertCountEqual(expected, collected)
+
+ class FakeMessageExtractor(devctl.I18nCubeMessageExtractor):
+ """Fake message extractor that generates no pot file."""
+
+ def generate_pot_file(self):
+ return None
+
+ @patch('pkg_resources.load_entry_point', return_value=FakeMessageExtractor)
+ def test_cube_custom_extractor(self, mock_load_entry_point):
+ for distname, cubedir in [
+ ('cubicweb_i18ntestcube',
+ osp.join(DATADIR, 'libpython', 'cubicweb_i18ntestcube')),
+ # Legacy cubes.
+ ('i18ntestcube', osp.join(DATADIR, 'cubes', 'i18ntestcube')),
+ ]:
+ with self.subTest(cubedir=cubedir):
+ with capture_stdout() as stream:
+ devctl.update_cube_catalogs(cubedir)
+ self.assertIn(u'no message catalog for cube i18ntestcube',
+ stream.read())
+ mock_load_entry_point.assert_called_once_with(
+ distname, 'cubicweb.i18ncube', 'i18ntestcube')
+ mock_load_entry_point.reset_mock()
+
+
if __name__ == '__main__':
main()
--- a/doc/book/devweb/internationalization.rst Tue Sep 27 12:28:39 2016 +0200
+++ b/doc/book/devweb/internationalization.rst Fri Oct 21 18:10:15 2016 +0200
@@ -153,6 +153,61 @@
3. `hg ci -m "updated i18n catalogs"`
4. `cubicweb-ctl i18ninstance <myinstance>`
+
+Customizing the messages extraction process
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The messages extraction performed by the ``i18ncommand`` collects messages
+from a few different sources:
+
+- the schema and application definition (entity names, docstrings,
+ help messages, uicfg),
+
+- the source files:
+
+ - ``i18n:content`` or ``i18n:replace`` directives from TAL files (with ``.pt`` extension),
+ - strings prefixed by an underscore (``_``) in python files,
+ - strings with double quotes prefixed by an underscore in javascript files.
+
+The source files are collected by walking through the cube directory,
+but ignoring a few directories like ``.hg``, ``.tox``, ``test`` or
+``node_modules``.
+
+If you need to customize this behaviour in your cube, you have to
+extend the ``cubicweb.devtools.devctl.I18nCubeMessageExtractor``. The
+example below will collect strings from ``jinja2`` files and ignore
+the ``static`` directory during the messages collection phase::
+
+ # mymodule.py
+ from cubicweb.devtools import devctl
+
+ class MyMessageExtractor(devctl.I18nCubeMessageExtractor):
+
+ blacklist = devctl.I18nCubeMessageExtractor | {'static'}
+ formats = devctl.I18nCubeMessageExtractor.formats + ['jinja2']
+
+ def collect_jinja2(self):
+ return self.find('.jinja2')
+
+ def extract_jinja2(self, files):
+ return self._xgettext(files, output='jinja.pot',
+ extraopts='-L python --from-code=utf-8')
+
+Then, you'll have to register it with a ``cubicweb.i18ncube`` entry point
+in your cube's setup.py::
+
+ setup(
+ # ...
+ entry_points={
+ # ...
+ 'cubicweb.i18ncube': [
+ 'mycube=cubicweb_mycube.mymodule:MyMessageExtractor',
+ ],
+ },
+ # ...
+ )
+
+
Editing po files
~~~~~~~~~~~~~~~~