Commit 1c4170ae authored by Adrien Di Mascio's avatar Adrien Di Mascio
Browse files

[devtools] make i18ncube customizable in a cube

closes #15613724
parent 7e2c2354dc99
......@@ -29,9 +29,11 @@ import tempfile
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 @@ def update_cube_catalogs(cubedir):
cubedir = osp.abspath(osp.normpath(cubedir))
workdir = tempfile.mkdtemp()
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
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))
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=' ')
......@@ -87,6 +87,9 @@ msgctxt "Forum"
msgid "description_format"
msgstr ""
msgid "ignore-me"
msgstr ""
msgid "in_forum"
msgstr ""
......@@ -18,13 +18,19 @@
# with CubicWeb. If not, see <>.
"""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 @@ def load_po(fname):
return msgs
TESTCUBE_DIR = osp.join(DATADIR, 'cubes', 'i18ntestcube')
class cubePotGeneratorTC(TestCase):
"""test case for i18n pot file generator"""
......@@ -87,5 +96,61 @@ class cubePotGeneratorTC(TestCase):
self.assertEqual(msgs, newmsgs)
class CustomMessageExtractor(devctl.I18nCubeMessageExtractor):
blacklist = devctl.I18nCubeMessageExtractor.blacklist | set(['excludeme'])
def capture_stdout():
stream = BytesIO() if PY2 else StringIO()
sys.stdout = stream
yield stream
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 ('', '',
'', '')]
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 [
osp.join(DATADIR, 'libpython', 'cubicweb_i18ntestcube')),
# Legacy cubes.
('i18ntestcube', osp.join(DATADIR, 'cubes', 'i18ntestcube')),
with self.subTest(cubedir=cubedir):
with capture_stdout() as stream:
self.assertIn(u'no message catalog for cube i18ntestcube',
distname, 'cubicweb.i18ncube', 'i18ntestcube')
if __name__ == '__main__':
......@@ -153,6 +153,61 @@ To update the translation catalogs you need to do:
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
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::
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
# ...
# ...
'cubicweb.i18ncube': [
# ...
Editing po files
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment