18 """additional cubicweb-ctl commands and command handlers for cubicweb and |
18 """additional cubicweb-ctl commands and command handlers for cubicweb and |
19 cubicweb's cubes development |
19 cubicweb's cubes development |
20 """ |
20 """ |
21 from __future__ import print_function |
21 from __future__ import print_function |
22 |
22 |
23 __docformat__ = "restructuredtext en" |
|
24 |
|
25 # *ctl module should limit the number of import to be imported as quickly as |
23 # *ctl module should limit the number of import to be imported as quickly as |
26 # possible (for cubicweb-ctl reactivity, necessary for instance for usable bash |
24 # possible (for cubicweb-ctl reactivity, necessary for instance for usable bash |
27 # completion). So import locally in command helpers. |
25 # completion). So import locally in command helpers. |
|
26 |
|
27 import shutil |
|
28 import tempfile |
28 import sys |
29 import sys |
29 from datetime import datetime, date |
30 from datetime import datetime, date |
30 from os import mkdir, chdir, path as osp |
31 from os import mkdir, chdir, path as osp |
31 from warnings import warn |
32 from warnings import warn |
32 |
33 |
33 from pytz import UTC |
34 from pytz import UTC |
34 from six.moves import input |
35 from six.moves import input |
35 |
36 |
36 from logilab.common import STD_BLACKLIST |
37 from logilab.common import STD_BLACKLIST |
|
38 from logilab.common.fileutils import ensure_fs_mode |
|
39 from logilab.common.shellutils import find |
37 |
40 |
38 from cubicweb.__pkginfo__ import version as cubicwebversion |
41 from cubicweb.__pkginfo__ import version as cubicwebversion |
39 from cubicweb import CW_SOFTWARE_ROOT as BASEDIR, BadCommandUsage, ExecutionError |
42 from cubicweb import CW_SOFTWARE_ROOT as BASEDIR, BadCommandUsage, ExecutionError |
|
43 from cubicweb.i18n import extract_from_tal, execute2 |
40 from cubicweb.cwctl import CWCTL |
44 from cubicweb.cwctl import CWCTL |
41 from cubicweb.cwconfig import CubicWebNoAppConfiguration |
45 from cubicweb.cwconfig import CubicWebNoAppConfiguration |
42 from cubicweb.toolsutils import (SKEL_EXCLUDE, Command, copy_skeleton, |
46 from cubicweb.toolsutils import (SKEL_EXCLUDE, Command, copy_skeleton, |
43 underline_title) |
47 underline_title) |
44 from cubicweb.web.webconfig import WebConfiguration |
48 from cubicweb.web.webconfig import WebConfiguration |
45 from cubicweb.server.serverconfig import ServerConfiguration |
49 from cubicweb.server.serverconfig import ServerConfiguration |
|
50 |
|
51 |
|
52 __docformat__ = "restructuredtext en" |
46 |
53 |
47 |
54 |
48 STD_BLACKLIST = set(STD_BLACKLIST) |
55 STD_BLACKLIST = set(STD_BLACKLIST) |
49 STD_BLACKLIST.add('.tox') |
56 STD_BLACKLIST.add('.tox') |
50 STD_BLACKLIST.add('test') |
57 STD_BLACKLIST.add('test') |
430 print('* ' + '\n* '.join(toedit)) |
437 print('* ' + '\n* '.join(toedit)) |
431 print ('When you are done, run "cubicweb-ctl i18ninstance ' |
438 print ('When you are done, run "cubicweb-ctl i18ninstance ' |
432 '<yourinstance>" to see changes in your instances.') |
439 '<yourinstance>" to see changes in your instances.') |
433 return True |
440 return True |
434 |
441 |
|
442 |
|
443 class I18nCubeMessageExtractor(object): |
|
444 """This class encapsulates all the xgettext extraction logic |
|
445 |
|
446 ``generate_pot_file`` is the main entry point called by the ``i18ncube`` |
|
447 command. A cube might decide to customize extractors to ignore a given |
|
448 directory or to extract messages from a new file type (e.g. .jinja2 files) |
|
449 |
|
450 For each file type, the class must define two methods: |
|
451 |
|
452 - ``collect_{filetype}()`` that must return the list of files |
|
453 xgettext should inspect, |
|
454 |
|
455 - ``extract_{filetype}(files)`` that calls xgettext and returns the |
|
456 path to the generated ``pot`` file |
|
457 """ |
|
458 blacklist = STD_BLACKLIST |
|
459 formats = ['tal', 'js', 'py'] |
|
460 |
|
461 def __init__(self, workdir, cubedir): |
|
462 self.workdir = workdir |
|
463 self.cubedir = cubedir |
|
464 |
|
465 def generate_pot_file(self): |
|
466 """main entry point: return the generated ``cube.pot`` file |
|
467 |
|
468 This function first generates all the pot files (schema, tal, |
|
469 py, js) and then merges them in a single ``cube.pot`` that will |
|
470 be used to eventually update the ``i18n/*.po`` files. |
|
471 """ |
|
472 potfiles = self.generate_pot_files() |
|
473 potfile = osp.join(self.workdir, 'cube.pot') |
|
474 print('-> merging %i .pot files' % len(potfiles)) |
|
475 cmd = ['msgcat', '-o', potfile] |
|
476 cmd.extend(potfiles) |
|
477 execute2(cmd) |
|
478 return potfile if osp.exists(potfile) else None |
|
479 |
|
480 def find(self, exts, blacklist=None): |
|
481 """collect files with extensions ``exts`` in the cube directory |
|
482 """ |
|
483 if blacklist is None: |
|
484 blacklist = self.blacklist |
|
485 return find(self.cubedir, exts, blacklist=blacklist) |
|
486 |
|
487 def generate_pot_files(self): |
|
488 """generate and return the list of all ``pot`` files for the cube |
|
489 |
|
490 - static-messages.pot, |
|
491 - schema.pot, |
|
492 - one ``pot`` file for each inspected format (.py, .js, etc.) |
|
493 """ |
|
494 print('-> extracting messages:', end=' ') |
|
495 potfiles = [] |
|
496 # static messages |
|
497 if osp.exists(osp.join('i18n', 'entities.pot')): |
|
498 warn('entities.pot is deprecated, rename file ' |
|
499 'to static-messages.pot (%s)' |
|
500 % osp.join('i18n', 'entities.pot'), DeprecationWarning) |
|
501 potfiles.append(osp.join('i18n', 'entities.pot')) |
|
502 elif osp.exists(osp.join('i18n', 'static-messages.pot')): |
|
503 potfiles.append(osp.join('i18n', 'static-messages.pot')) |
|
504 # messages from schema |
|
505 potfiles.append(self.schemapot()) |
|
506 # messages from sourcecode |
|
507 for fmt in self.formats: |
|
508 collector = getattr(self, 'collect_{0}'.format(fmt)) |
|
509 extractor = getattr(self, 'extract_{0}'.format(fmt)) |
|
510 files = collector() |
|
511 if files: |
|
512 potfile = extractor(files) |
|
513 if potfile: |
|
514 potfiles.append(potfile) |
|
515 return potfiles |
|
516 |
|
517 def schemapot(self): |
|
518 """generate the ``schema.pot`` file""" |
|
519 schemapot = osp.join(self.workdir, 'schema.pot') |
|
520 print('schema', end=' ') |
|
521 # explicit close necessary else the file may not be yet flushed when |
|
522 # we'll using it below |
|
523 schemapotstream = open(schemapot, 'w') |
|
524 generate_schema_pot(schemapotstream.write, self.cubedir) |
|
525 schemapotstream.close() |
|
526 return schemapot |
|
527 |
|
528 def _xgettext(self, files, output, k='_', extraopts=''): |
|
529 """shortcut to execute the xgettext command and return output file |
|
530 """ |
|
531 tmppotfile = osp.join(self.workdir, output) |
|
532 cmd = ['xgettext', '--no-location', '--omit-header', '-k' + k, |
|
533 '-o', tmppotfile] + extraopts.split() + files |
|
534 execute2(cmd) |
|
535 if osp.exists(tmppotfile): |
|
536 return tmppotfile |
|
537 |
|
538 def collect_tal(self): |
|
539 print('TAL', end=' ') |
|
540 return self.find(('.py', '.pt')) |
|
541 |
|
542 def extract_tal(self, files): |
|
543 tali18nfile = osp.join(self.workdir, 'tali18n.py') |
|
544 extract_from_tal(files, tali18nfile) |
|
545 return self._xgettext(files, output='tal.pot') |
|
546 |
|
547 def collect_js(self): |
|
548 print('Javascript') |
|
549 return [jsfile for jsfile in self.find('.js') |
|
550 if osp.basename(jsfile).startswith('cub')] |
|
551 |
|
552 def extract_js(self, files): |
|
553 return self._xgettext(files, output='js.pot', |
|
554 extraopts='-L java --from-code=utf-8') |
|
555 |
|
556 def collect_py(self): |
|
557 print('-> creating cube-specific catalog') |
|
558 return self.find('.py') |
|
559 |
|
560 def extract_py(self, files): |
|
561 return self._xgettext(files, output='py.pot') |
|
562 |
|
563 |
435 def update_cube_catalogs(cubedir): |
564 def update_cube_catalogs(cubedir): |
436 import shutil |
565 cubedir = osp.abspath(osp.normpath(cubedir)) |
437 import tempfile |
566 workdir = tempfile.mkdtemp() |
438 from logilab.common.fileutils import ensure_fs_mode |
567 cube = osp.basename(cubedir) |
439 from logilab.common.shellutils import find, rm |
568 print('cubedir', cubedir) |
440 from cubicweb.i18n import extract_from_tal, execute2 |
|
441 cube = osp.basename(osp.normpath(cubedir)) |
|
442 tempdir = tempfile.mkdtemp() |
|
443 print(underline_title('Updating i18n catalogs for cube %s' % cube)) |
569 print(underline_title('Updating i18n catalogs for cube %s' % cube)) |
444 chdir(cubedir) |
570 chdir(cubedir) |
445 if osp.exists(osp.join('i18n', 'entities.pot')): |
571 extractor = I18nCubeMessageExtractor(workdir, cubedir) |
446 warn('entities.pot is deprecated, rename file to static-messages.pot (%s)' |
572 potfile = extractor.generate_pot_file() |
447 % osp.join('i18n', 'entities.pot'), DeprecationWarning) |
573 if potfile is None: |
448 potfiles = [osp.join('i18n', 'entities.pot')] |
|
449 elif osp.exists(osp.join('i18n', 'static-messages.pot')): |
|
450 potfiles = [osp.join('i18n', 'static-messages.pot')] |
|
451 else: |
|
452 potfiles = [] |
|
453 print('-> extracting messages:', end=' ') |
|
454 print('schema', end=' ') |
|
455 schemapot = osp.join(tempdir, 'schema.pot') |
|
456 potfiles.append(schemapot) |
|
457 # explicit close necessary else the file may not be yet flushed when |
|
458 # we'll using it below |
|
459 schemapotstream = open(schemapot, 'w') |
|
460 generate_schema_pot(schemapotstream.write, cubedir) |
|
461 schemapotstream.close() |
|
462 print('TAL', end=' ') |
|
463 tali18nfile = osp.join(tempdir, 'tali18n.py') |
|
464 ptfiles = find('.', ('.py', '.pt'), blacklist=STD_BLACKLIST) |
|
465 extract_from_tal(ptfiles, tali18nfile) |
|
466 print('Javascript') |
|
467 jsfiles = [jsfile for jsfile in find('.', '.js') |
|
468 if osp.basename(jsfile).startswith('cub')] |
|
469 if jsfiles: |
|
470 tmppotfile = osp.join(tempdir, 'js.pot') |
|
471 cmd = ['xgettext', '--no-location', '--omit-header', '-k_', '-L', 'java', |
|
472 '--from-code=utf-8', '-o', tmppotfile] + jsfiles |
|
473 execute2(cmd) |
|
474 # no pot file created if there are no string to translate |
|
475 if osp.exists(tmppotfile): |
|
476 potfiles.append(tmppotfile) |
|
477 print('-> creating cube-specific catalog') |
|
478 tmppotfile = osp.join(tempdir, 'generated.pot') |
|
479 cubefiles = find('.', '.py', blacklist=STD_BLACKLIST) |
|
480 cubefiles.append(tali18nfile) |
|
481 cmd = ['xgettext', '--no-location', '--omit-header', '-k_', '-o', tmppotfile] |
|
482 cmd.extend(cubefiles) |
|
483 execute2(cmd) |
|
484 if osp.exists(tmppotfile): # doesn't exists of no translation string found |
|
485 potfiles.append(tmppotfile) |
|
486 potfile = osp.join(tempdir, 'cube.pot') |
|
487 print('-> merging %i .pot files' % len(potfiles)) |
|
488 cmd = ['msgcat', '-o', potfile] |
|
489 cmd.extend(potfiles) |
|
490 execute2(cmd) |
|
491 if not osp.exists(potfile): |
|
492 print('no message catalog for cube', cube, 'nothing to translate') |
574 print('no message catalog for cube', cube, 'nothing to translate') |
493 # cleanup |
575 shutil.rmtree(workdir) |
494 rm(tempdir) |
|
495 return () |
576 return () |
496 print('-> merging main pot file with existing translations:', end=' ') |
577 print('-> merging main pot file with existing translations:', end=' ') |
497 chdir('i18n') |
578 chdir('i18n') |
498 toedit = [] |
579 toedit = [] |
499 for lang in CubicWebNoAppConfiguration.cw_languages(): |
580 for lang in CubicWebNoAppConfiguration.cw_languages(): |
500 print(lang, end=' ') |
581 print(lang, end=' ') |
501 cubepo = '%s.po' % lang |
582 cubepo = '%s.po' % lang |
502 if not osp.exists(cubepo): |
583 if not osp.exists(cubepo): |
503 shutil.copy(potfile, cubepo) |
584 shutil.copy(potfile, cubepo) |
504 else: |
585 else: |
505 cmd = ['msgmerge','-N','-s','-o', cubepo+'new', cubepo, potfile] |
586 cmd = ['msgmerge', '-N', '-s', '-o', cubepo + 'new', |
|
587 cubepo, potfile] |
506 execute2(cmd) |
588 execute2(cmd) |
507 ensure_fs_mode(cubepo) |
589 ensure_fs_mode(cubepo) |
508 shutil.move('%snew' % cubepo, cubepo) |
590 shutil.move('%snew' % cubepo, cubepo) |
509 toedit.append(osp.abspath(cubepo)) |
591 toedit.append(osp.abspath(cubepo)) |
510 print() |
592 print() |
511 # cleanup |
593 # cleanup |
512 rm(tempdir) |
594 shutil.rmtree(workdir) |
513 return toedit |
595 return toedit |
514 |
596 |
515 |
597 |
516 class NewCubeCommand(Command): |
598 class NewCubeCommand(Command): |
517 """Create a new cube. |
599 """Create a new cube. |