Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
fun-apps / management / commands / makemessages_fun.py
Size: Mime:
#! /usr/bin/env python
# -*- coding: utf-8 -*-
import argparse
from datetime import datetime
import os
import optparse
import sys
import tempfile

from babel.messages.pofile import read_po, write_po
from babel.messages.frontend import CommandLineInterface as BabelCommandLineInterface

try:
    from django.core.management.base import BaseCommand
except ImportError:
    class BaseCommand(object):
        option_list = ()

CURRENT_DIR = os.path.dirname(__file__)
FUN_APPS_ROOT_DIR = os.path.abspath(os.path.join(CURRENT_DIR, "../../../"))
PATH_FUN_PO = "%(root_path)s/locale/%(locale)s/LC_MESSAGES/%(domain)s.po"
PATH_EDX_PO = os.path.join(
    FUN_APPS_ROOT_DIR, "../edx-platform/",
    "conf/locale/%(locale)s/LC_MESSAGES/%(domain)s.po"
)


class Command(BaseCommand):

    help = """Usage: makemessages_fun [-v] [-l fr|de] path1 [path2 [...]]

Update the %s files to make sure all fun-apps translations are
up-to-date.

You may also pass the 'all' app name to gather messages for all applications.

After you have collected the messages for an app, don't forget to run the
following command in the app folder to make sure the new translations are
properly compiled:

    django-admin.py compilemessages -l <locale>
""" % PATH_FUN_PO

    option_list = BaseCommand.option_list + (
        optparse.make_option('-l', '--locale',
            action="store",
            choices=("fr", "de"),
            default="fr",
            help="Select locale to process."),
        optparse.make_option('--compile',
            action="store_true",
            default=False,
            help="Run compilemessages after message collection."),
        optparse.make_option('--verbose',
            action="store_true",
            default=False,
            help="Activate verbose mode."),
    )

    def handle(self, *args, **options):
        locale = options["locale"]
        is_verbose = options["verbose"] or int(options["verbosity"]) > 1
        run_compile = options["compile"]
        MessageMaker(self.stdout, is_verbose=is_verbose).handle(args, locale, run_compile=run_compile)

class MessageMaker(object):

    def __init__(self, stdout=None, is_verbose=False, run_compile=False):
        self.stdout = stdout or sys.stdout
        self.is_verbose = is_verbose
        self.run_compile = run_compile
        self._edx_catalogs = {}

    def edx_catalog(self, locale, domain):
        key = (locale, domain)
        if key not in self._edx_catalogs:
            self._edx_catalogs[key] = read_po_catalog(
                PATH_EDX_PO % {'locale': locale, 'domain': domain},
                locale
            )
        return self._edx_catalogs[key]

    def handle(self, root_paths, locale, run_compile=False):
        for root_path in root_paths:
            if root_path == "all":
                self.make_all_messages(locale, run_compile=run_compile)
            else:
                self.make_messages(os.path.abspath(root_path), locale, run_compile=run_compile)

    def make_all_messages(self, locale, run_compile=False):
        # Translate apps
        from django.conf import settings
        fun_apps_to_translate = settings.LOCALIZED_APPS
        for app_name in fun_apps_to_translate:
            self.make_messages(app_root_path(app_name), locale, run_compile=run_compile)

    def make_messages(self, root_path, locale, run_compile=False):
        self.make_domain_messages(root_path, locale, 'django')
        self.make_domain_messages(root_path, locale, 'djangojs')

        if run_compile:
            compile_messages(root_path, locale)

    def make_domain_messages(self, root_path, locale, domain):
        path_fun_po = fun_app_po_path(root_path, locale, domain)
        if not os.path.exists(path_fun_po):
            print "Skipping {} because file does not exist".format(path_fun_po)
            return

        # Catalog of required translations
        pot_catalog = make_pot_catalog(root_path, domain=domain)

        # Catalog of fun translations
        fun_catalog = read_po_catalog(path_fun_po, locale)

        # Catalog of edx translations
        edx_catalog = self.edx_catalog(locale, domain)

        # Merge FUN and edX catalogs
        update_catalog(fun_catalog, edx_catalog, pot_catalog)

        if self.is_verbose:
            # Verify catalog
            check_catalog(fun_catalog)

        # Fix headers and message locations
        fix_catalog_properties(fun_catalog)
        remove_line_numbers(fun_catalog)

        # Save result
        self.stdout.write("Updating %s...\n" % path_fun_po)
        write_po_catalog(fun_catalog, path_fun_po)

def app_root_path(app_name):
    return os.path.join(FUN_APPS_ROOT_DIR, app_name)

def fun_app_po_path(root_path, locale, domain):
    return PATH_FUN_PO % {"locale": locale, "root_path": root_path, 'domain': domain}

def make_pot_catalog(root_path, domain='django'):
    pot_path = os.path.join(tempfile.gettempdir(), "fun-{}.pot".format(domain))
    cfg_path = os.path.join(FUN_APPS_ROOT_DIR, "fun/locale", "babel-{}.cfg".format(domain))
    extract_command_args = [
        "pybabel", "--quiet", "extract",
        "-o", pot_path, "-F", cfg_path,
        "--keyword=pgettext_lazy:1c,2",
        root_path
    ]
    BabelCommandLineInterface().run(extract_command_args)
    pot_catalog = read_po_catalog(pot_path, None)
    return pot_catalog

def read_po_catalog(path, locale):
    with open(path) as po_file:
        return read_po(po_file, locale=locale)

def update_catalog(fun_catalog, edx_catalog, pot_catalog):
    """
    Update fun_catalog with the following strategy:
        - get rid of messages from pot_catalog that are already translated in edx_catalog (but not in fun_catalog).
        - add a comment to fun_catalog messages that override existing edx messages
        - update the existing fun_catalog with the messages from pot_catalog (the new catalog)
        - make sure that translations from fun_catalog that override edx_catalog messages are kept
    """
    remove_messages_that_should_not_be_translated(pot_catalog, fun_catalog, edx_catalog)
    comment_messages(pot_catalog, edx_catalog)
    comment_messages(fun_catalog, edx_catalog)
    fun_catalog.update(pot_catalog)
    keep_overriden_messages(fun_catalog, edx_catalog)

def remove_messages_that_should_not_be_translated(pot_catalog, fun_catalog, edx_catalog):
    # Filter out messages that are translated in edx_catalog but not in fun_catalog
    message_ids_to_delete = []
    for message in pot_catalog:
        if message.id not in fun_catalog and message.id in edx_catalog:
            message_ids_to_delete.append(message.id)
    for message_id in message_ids_to_delete:
        pot_catalog.delete(message_id)

def comment_messages(catalog, edx_catalog):
    for message in catalog:
        message.user_comments = []
        if message.id in edx_catalog:
            message.user_comments.append("Translated in edx by '%s'" % edx_catalog[message.id].string)

def keep_overriden_messages(fun_catalog, edx_catalog):
    # Keep obsolete messages that actually override edx messages
    overriden_message_ids = set()
    for message_id, message in fun_catalog.obsolete.iteritems():
        if message_id in edx_catalog and message.string != edx_catalog[message_id].string:
            overriden_message_ids.add(message_id)

    for message_id in overriden_message_ids:
        # default=None is actually required here because of a quirkness in how odict.pop works
        try:
            fun_catalog[message_id] = fun_catalog.obsolete.pop(message_id, default=None)
        except:
            print (
                "Error while processing message {}. Please check if the message "
                "still exists in the application and should not be removed from "
                "the .po file. Note that context and pluralization is not "
                "supported by babel in Django templates."
            ).format(message_id)
            raise

def check_catalog(catalog):
    missing_translations = sorted([
        message.id for message in catalog
        if not message.string
    ])
    if missing_translations:
        print "{} missing translations:".format(len(missing_translations))
        for missing_translation in missing_translations:
            print u"    - {}".format(missing_translation)

def fix_catalog_properties(catalog):
    """Set FUn-compliant catalog properties"""
    catalog.header_comment = ""
    catalog.project = u"FUN-MOOC"
    catalog.copyright_holder = u"FUN-MOOC"
    catalog.revision_date = datetime.now()
    try:
        catalog.version = str(int(catalog.version) + 1)
    except ValueError:
        catalog.version = "0"
    catalog.msgid_bugs_address = "team@openfun.fr"
    catalog.language_team = "FUN <team@openfun.fr>"
    catalog.last_translator = catalog.language_team

def remove_line_numbers(catalog):
    """Replace line numbers by 0

    Line numbers need to be integers to make formatting work. We don't want to
    keep line numbers because they complicate pull requests. So we replace them
    by a fixed value ('0').
    """
    for message in catalog:
        message.locations = sorted(set([(path, 0) for (path, _lineno) in message.locations]))

def write_po_catalog(catalog, path):
    with open(path, "w") as catalog_file:
        write_po(catalog_file, catalog, sort_output=True)

def compile_messages(path, locale):
    from fun.utils.context import cd, setenv
    import subprocess
    with cd(path):
        with setenv("DJANGO_SETTINGS_MODULE", None):
            subprocess.call(["/edx/app/edxapp/venvs/edxapp/bin/django-admin.py", "compilemessages", "-l", locale])

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Make messages to translate for FUN")
    parser.add_argument("locale", choices=['fr', 'de'], help="Locale to translate to")
    parser.add_argument("paths", nargs='+', help="Paths to process")

    main_args = parser.parse_args()

    MessageMaker().handle(main_args.paths, main_args.locale)