manage_translations.py 6.92 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
#!/usr/bin/env python
#
# This python file contains utility scripts to manage Django translations.
# It has to be run inside the django git root directory.
#
# The following commands are available:
#
# * update_catalogs: check for new strings in core and contrib catalogs, and
#                    output how much strings are new/changed.
#
# * lang_stats: output statistics for each catalog/language combination
#
# * fetch: fetch translations from transifex.com
#
# Each command support the --languages and --resources options to limit their
# operation to the specified language or resource. For example, to get stats
# for Spanish in contrib.admin, run:
#
#  $ python scripts/manage_translations.py lang_stats --language=es --resources=admin

21
from argparse import ArgumentParser
22 23 24 25 26 27 28 29
import os
from subprocess import call, Popen, PIPE

from django.core.management import call_command


HAVE_JS = ['admin']

30

31
def _get_locale_dirs(resources, include_core=True):
32 33 34
    """
    Return a tuple (contrib name, absolute path) for all locale directories,
    optionally including the django core catalog.
35
    If resources list is not None, filter directories matching resources content.
36 37 38
    """
    contrib_dir = os.path.join(os.getcwd(), 'django', 'contrib')
    dirs = []
39 40

    # Collect all locale directories
41 42 43 44 45 46 47 48
    for contrib_name in os.listdir(contrib_dir):
        path = os.path.join(contrib_dir, contrib_name, 'locale')
        if os.path.isdir(path):
            dirs.append((contrib_name, path))
            if contrib_name in HAVE_JS:
                dirs.append(("%s-js" % contrib_name, path))
    if include_core:
        dirs.insert(0, ('core', os.path.join(os.getcwd(), 'django', 'conf', 'locale')))
49 50 51 52 53 54 55 56 57

    # Filter by resources, if any
    if resources is not None:
        res_names = [d[0] for d in dirs]
        dirs = [ld for ld in dirs if ld[0] in resources]
        if len(resources) > len(dirs):
            print("You have specified some unknown resources. "
                  "Available resource names are: %s" % (', '.join(res_names),))
            exit(1)
58 59
    return dirs

60

61 62 63
def _tx_resource_for_name(name):
    """ Return the Transifex resource name """
    if name == 'core':
64
        return "django.core"
65
    else:
66
        return "django.contrib-%s" % name
67

68

69 70 71 72 73 74
def _check_diff(cat_name, base_path):
    """
    Output the approximate number of changed/added strings in the en catalog.
    """
    po_path = '%(path)s/en/LC_MESSAGES/django%(ext)s.po' % {
        'path': base_path, 'ext': 'js' if cat_name.endswith('-js') else ''}
75
    p = Popen("git diff -U0 %s | egrep '^[-+]msgid' | wc -l" % po_path,
76 77
              stdout=PIPE, stderr=PIPE, shell=True)
    output, errors = p.communicate()
78
    num_changes = int(output.strip())
79 80 81 82 83 84 85 86
    print("%d changed/added messages in '%s' catalog." % (num_changes, cat_name))


def update_catalogs(resources=None, languages=None):
    """
    Update the en/LC_MESSAGES/django.po (main and contrib) files with
    new/updated translatable strings.
    """
87 88 89
    if resources is not None:
        print("`update_catalogs` will always process all resources.")
    contrib_dirs = _get_locale_dirs(None, include_core=False)
90 91

    os.chdir(os.path.join(os.getcwd(), 'django'))
92
    print("Updating en catalogs for Django and contrib apps...")
93
    call_command('makemessages', locale=['en'])
94 95
    print("Updating en JS catalogs for Django and contrib apps...")
    call_command('makemessages', locale=['en'], domain='djangojs')
96

97 98
    # Output changed stats
    _check_diff('core', os.path.join(os.getcwd(), 'conf', 'locale'))
99 100 101 102 103 104 105 106 107 108 109
    for name, dir_ in contrib_dirs:
        _check_diff(name, dir_)


def lang_stats(resources=None, languages=None):
    """
    Output language statistics of committed translation files for each
    Django catalog.
    If resources is provided, it should be a list of translation resource to
    limit the output (e.g. ['core', 'gis']).
    """
110
    locale_dirs = _get_locale_dirs(resources)
111 112

    for name, dir_ in locale_dirs:
Tim Graham's avatar
Tim Graham committed
113
        print("\nShowing translations stats for '%s':" % name)
114 115
        langs = sorted([d for d in os.listdir(dir_) if not d.startswith('_')])
        for lang in langs:
116
            if languages and lang not in languages:
117 118 119 120 121 122 123 124 125
                continue
            # TODO: merge first with the latest en catalog
            p = Popen("msgfmt -vc -o /dev/null %(path)s/%(lang)s/LC_MESSAGES/django%(ext)s.po" % {
                'path': dir_, 'lang': lang, 'ext': 'js' if name.endswith('-js') else ''},
                stdout=PIPE, stderr=PIPE, shell=True)
            output, errors = p.communicate()
            if p.returncode == 0:
                # msgfmt output stats on stderr
                print("%s: %s" % (lang, errors.strip()))
126 127 128
            else:
                print("Errors happened when checking %s translation for %s:\n%s" % (
                    lang, name, errors))
129 130 131 132 133 134


def fetch(resources=None, languages=None):
    """
    Fetch translations from Transifex, wrap long lines, generate mo files.
    """
135
    locale_dirs = _get_locale_dirs(resources)
136
    errors = []
137 138 139 140

    for name, dir_ in locale_dirs:
        # Transifex pull
        if languages is None:
141 142
            call('tx pull -r %(res)s -a -f  --minimum-perc=5' % {'res': _tx_resource_for_name(name)}, shell=True)
            languages = sorted([d for d in os.listdir(dir_) if not d.startswith('_') and d != 'en'])
143 144 145 146 147 148 149 150 151
        else:
            for lang in languages:
                call('tx pull -r %(res)s -f -l %(lang)s' % {
                    'res': _tx_resource_for_name(name), 'lang': lang}, shell=True)

        # msgcat to wrap lines and msgfmt for compilation of .mo file
        for lang in languages:
            po_path = '%(path)s/%(lang)s/LC_MESSAGES/django%(ext)s.po' % {
                'path': dir_, 'lang': lang, 'ext': 'js' if name.endswith('-js') else ''}
152 153 154 155
            if not os.path.exists(po_path):
                print("No %(lang)s translation for resource %(name)s" % {
                    'lang': lang, 'name': name})
                continue
156
            call('msgcat -o %s %s' % (po_path, po_path), shell=True)
157 158 159 160 161 162 163 164
            res = call('msgfmt -c -o %s.mo %s' % (po_path[:-3], po_path), shell=True)
            if res != 0:
                errors.append((name, lang))
    if errors:
        print("\nWARNING: Errors have occurred in following cases:")
        for resource, lang in errors:
            print("\tResource %s for language %s" % (resource, lang))
        exit(1)
165 166 167 168 169


if __name__ == "__main__":
    RUNABLE_SCRIPTS = ('update_catalogs', 'lang_stats', 'fetch')

170 171 172
    parser = ArgumentParser()
    parser.add_argument('cmd', nargs=1)
    parser.add_argument("-r", "--resources", action='append',
173
        help="limit operation to the specified resources")
174
    parser.add_argument("-l", "--languages", action='append',
175
        help="limit operation to the specified languages")
176
    options = parser.parse_args()
177

178 179
    if options.cmd[0] in RUNABLE_SCRIPTS:
        eval(options.cmd[0])(options.resources, options.languages)
180 181
    else:
        print("Available commands are: %s" % ", ".join(RUNABLE_SCRIPTS))