adding translations and transifex third party tool
[pub/Android/ownCloud.git] / third_party / transifex-client / txclib / commands.py
diff --git a/third_party/transifex-client/txclib/commands.py b/third_party/transifex-client/txclib/commands.py
new file mode 100644 (file)
index 0000000..282287f
--- /dev/null
@@ -0,0 +1,576 @@
+# -*- coding: utf-8 -*-
+"""
+In this file we have all the top level commands for the transifex client.
+Since we're using a way to automatically list them and execute them, when
+adding code to this file you must take care of the following:
+ * Added functions must begin with 'cmd_' followed by the actual name of the
+   command being used in the command line (eg cmd_init)
+ * The description for each function that we display to the user is read from
+   the func_doc attribute which reads the doc string. So, when adding
+   docstring to a new function make sure you add an oneliner which is
+   descriptive and is meant to be seen by the user.
+ * When including libraries, it's best if you include modules instead of
+   functions because that way our function resolution will work faster and the
+   chances of overlapping are minimal
+ * All functions should use the OptionParser and should have a usage and
+   descripition field.
+"""
+import os
+import re, shutil
+import sys
+from optparse import OptionParser, OptionGroup
+import ConfigParser
+
+
+from txclib import utils, project
+from txclib.utils import parse_json, compile_json, relpath
+from txclib.config import OrderedRawConfigParser
+from txclib.exceptions import UnInitializedError
+from txclib.parsers import delete_parser, help_parser, parse_csv_option, \
+        status_parser, pull_parser, set_parser, push_parser, init_parser
+from txclib.log import logger
+
+
+def cmd_init(argv, path_to_tx):
+    "Initialize a new transifex project."
+    parser = init_parser()
+    (options, args) = parser.parse_args(argv)
+    if len(args) > 1:
+        parser.error("Too many arguments were provided. Aborting...")
+    if args:
+        path_to_tx = args[0]
+    else:
+        path_to_tx = os.getcwd()
+
+    if os.path.isdir(os.path.join(path_to_tx,".tx")):
+        logger.info("tx: There is already a tx folder!")
+        reinit = raw_input("Do you want to delete it and reinit the project? [y/N]: ")
+        while (reinit != 'y' and reinit != 'Y' and reinit != 'N' and reinit != 'n' and reinit != ''):
+            reinit = raw_input("Do you want to delete it and reinit the project? [y/N]: ")
+        if not reinit or reinit in ['N', 'n', 'NO', 'no', 'No']:
+            return
+        # Clean the old settings
+        # FIXME: take a backup
+        else:
+            rm_dir = os.path.join(path_to_tx, ".tx")
+            shutil.rmtree(rm_dir)
+
+    logger.info("Creating .tx folder...")
+    os.mkdir(os.path.join(path_to_tx,".tx"))
+
+    # Handle the credentials through transifexrc
+    home = os.path.expanduser("~")
+    txrc = os.path.join(home, ".transifexrc")
+    config = OrderedRawConfigParser()
+
+    default_transifex = "https://www.transifex.com"
+    transifex_host = options.host or raw_input("Transifex instance [%s]: " % default_transifex)
+
+    if not transifex_host:
+        transifex_host = default_transifex
+    if not transifex_host.startswith(('http://', 'https://')):
+        transifex_host = 'https://' + transifex_host
+
+    config_file = os.path.join(path_to_tx, ".tx", "config")
+    if not os.path.exists(config_file):
+        # The path to the config file (.tx/config)
+        logger.info("Creating skeleton...")
+        config = OrderedRawConfigParser()
+        config.add_section('main')
+        config.set('main', 'host', transifex_host)
+        # Touch the file if it doesn't exist
+        logger.info("Creating config file...")
+        fh = open(config_file, 'w')
+        config.write(fh)
+        fh.close()
+
+    prj = project.Project(path_to_tx)
+    prj.getset_host_credentials(transifex_host, user=options.user,
+        password=options.password)
+    prj.save()
+    logger.info("Done.")
+
+
+def cmd_set(argv, path_to_tx):
+    "Add local or remote files under transifex"
+    parser = set_parser()
+    (options, args) = parser.parse_args(argv)
+
+    # Implement options/args checks
+    # TODO !!!!!!!
+    if options.local:
+        try:
+            expression = args[0]
+        except IndexError:
+            parser.error("Please specify an expression.")
+        if not options.resource:
+            parser.error("Please specify a resource")
+        if not options.source_language:
+            parser.error("Please specify a source language.")
+        if not '<lang>' in expression:
+            parser.error("The expression you have provided is not valid.")
+        if not utils.valid_slug(options.resource):
+            parser.error("Invalid resource slug. The format is <project_slug>"\
+                ".<resource_slug> and the valid characters include [_-\w].")
+        _auto_local(path_to_tx, options.resource,
+            source_language=options.source_language,
+            expression = expression, source_file=options.source_file,
+            execute=options.execute, regex=False)
+        if options.execute:
+            _set_minimum_perc(options.resource, options.minimum_perc, path_to_tx)
+            _set_mode(options.resource, options.mode, path_to_tx)
+            _set_type(options.resource, options.i18n_type, path_to_tx)
+        return
+
+    if options.remote:
+        try:
+            url = args[0]
+        except IndexError:
+            parser.error("Please specify an remote url")
+        _auto_remote(path_to_tx, url)
+        _set_minimum_perc(options.resource, options.minimum_perc, path_to_tx)
+        _set_mode(options.resource, options.mode, path_to_tx)
+        return
+
+    if options.is_source:
+        resource = options.resource
+        if not resource:
+            parser.error("You must specify a resource name with the"
+                " -r|--resource flag.")
+
+        lang = options.language
+        if not lang:
+            parser.error("Please specify a source language.")
+
+        if len(args) != 1:
+            parser.error("Please specify a file.")
+
+        if not utils.valid_slug(resource):
+            parser.error("Invalid resource slug. The format is <project_slug>"\
+                ".<resource_slug> and the valid characters include [_-\w].")
+
+        file = args[0]
+        # Calculate relative path
+        path_to_file = relpath(file, path_to_tx)
+        _set_source_file(path_to_tx, resource, options.language, path_to_file)
+    elif options.resource or options.language:
+        resource = options.resource
+        lang = options.language
+
+        if len(args) != 1:
+            parser.error("Please specify a file")
+
+        # Calculate relative path
+        path_to_file = relpath(args[0], path_to_tx)
+
+        try:
+            _go_to_dir(path_to_tx)
+        except UnInitializedError, e:
+            utils.logger.error(e)
+            return
+
+        if not utils.valid_slug(resource):
+            parser.error("Invalid resource slug. The format is <project_slug>"\
+                ".<resource_slug> and the valid characters include [_-\w].")
+        _set_translation(path_to_tx, resource, lang, path_to_file)
+
+    _set_mode(options.resource, options.mode, path_to_tx)
+    _set_type(options.resource, options.i18n_type, path_to_tx)
+    _set_minimum_perc(options.resource, options.minimum_perc, path_to_tx)
+
+    logger.info("Done.")
+    return
+
+
+def _auto_local(path_to_tx, resource, source_language, expression, execute=False,
+                source_file=None, regex=False):
+    """Auto configure local project."""
+    # The path everything will be relative to
+    curpath = os.path.abspath(os.curdir)
+
+    # Force expr to be a valid regex expr (escaped) but keep <lang> intact
+    expr_re = utils.regex_from_filefilter(expression, curpath)
+    expr_rec = re.compile(expr_re)
+
+    if not execute:
+        logger.info("Only printing the commands which will be run if the "
+                  "--execute switch is specified.")
+
+    # First, let's construct a dictionary of all matching files.
+    # Note: Only the last matching file of a language will be stored.
+    translation_files = {}
+    for root, dirs, files in os.walk(curpath):
+        for f in files:
+            f_path = os.path.abspath(os.path.join(root, f))
+            match = expr_rec.match(f_path)
+            if match:
+                lang = match.group(1)
+                f_path = os.path.abspath(f_path)
+                if lang == source_language and not source_file:
+                    source_file = f_path
+                else:
+                    translation_files[lang] = f_path
+
+    if not source_file:
+        raise Exception("Could not find a source language file. Please run"
+            " set --source manually and then re-run this command or provide"
+            " the source file with the -s flag.")
+    if execute:
+        logger.info("Updating source for resource %s ( %s -> %s )." % (resource,
+            source_language, relpath(source_file, path_to_tx)))
+        _set_source_file(path_to_tx, resource, source_language,
+            relpath(source_file, path_to_tx))
+    else:
+        logger.info('\ntx set --source -r %(res)s -l %(lang)s %(file)s\n' % {
+            'res': resource,
+            'lang': source_language,
+            'file': relpath(source_file, curpath)})
+
+    prj = project.Project(path_to_tx)
+    root_dir = os.path.abspath(path_to_tx)
+
+    if execute:
+        try:
+            prj.config.get("%s" % resource, "source_file")
+        except ConfigParser.NoSectionError:
+            raise Exception("No resource with slug \"%s\" was found.\nRun 'tx set --auto"
+                "-local -r %s \"expression\"' to do the initial configuration." % resource)
+
+    # Now let's handle the translation files.
+    if execute:
+        logger.info("Updating file expression for resource %s ( %s )." % (resource,
+            expression))
+        # Eval file_filter relative to root dir
+        file_filter = relpath(os.path.join(curpath, expression),
+            path_to_tx)
+        prj.config.set("%s" % resource, "file_filter", file_filter)
+    else:
+        for (lang, f_path) in sorted(translation_files.items()):
+            logger.info('tx set -r %(res)s -l %(lang)s %(file)s' % {
+                'res': resource,
+                'lang': lang,
+                'file': relpath(f_path, curpath)})
+
+    if execute:
+        prj.save()
+
+
+def _auto_remote(path_to_tx, url):
+    """
+    Initialize a remote release/project/resource to the current directory.
+    """
+    logger.info("Auto configuring local project from remote URL...")
+
+    type, vars = utils.parse_tx_url(url)
+    prj = project.Project(path_to_tx)
+    username, password = prj.getset_host_credentials(vars['hostname'])
+
+    if type == 'project':
+        logger.info("Getting details for project %s" % vars['project'])
+        proj_info = utils.get_details('project_details',
+            username, password,
+            hostname = vars['hostname'], project = vars['project'])
+        resources = [ '.'.join([vars['project'], r['slug']]) for r in proj_info['resources'] ]
+        logger.info("%s resources found. Configuring..." % len(resources))
+    elif type == 'release':
+        logger.info("Getting details for release %s" % vars['release'])
+        rel_info = utils.get_details('release_details',
+            username, password, hostname = vars['hostname'],
+            project = vars['project'], release = vars['release'])
+        resources = []
+        for r in rel_info['resources']:
+            if r.has_key('project'):
+                resources.append('.'.join([r['project']['slug'], r['slug']]))
+            else:
+                resources.append('.'.join([vars['project'], r['slug']]))
+        logger.info("%s resources found. Configuring..." % len(resources))
+    elif type == 'resource':
+        logger.info("Getting details for resource %s" % vars['resource'])
+        resources = [ '.'.join([vars['project'], vars['resource']]) ]
+    else:
+        raise("Url '%s' is not recognized." % url)
+
+    for resource in resources:
+        logger.info("Configuring resource %s." % resource)
+        proj, res = resource.split('.')
+        res_info = utils.get_details('resource_details',
+             username, password, hostname = vars['hostname'],
+             project = proj, resource=res)
+        try:
+            source_lang = res_info['source_language_code']
+            i18n_type = res_info['i18n_type']
+        except KeyError:
+            raise Exception("Remote server seems to be running an unsupported version"
+                " of Transifex. Either update your server software of fallback"
+                " to a previous version of transifex-client.")
+        prj.set_remote_resource(
+            resource=resource,
+            host = vars['hostname'],
+            source_lang = source_lang,
+            i18n_type = i18n_type)
+
+    prj.save()
+
+
+def cmd_push(argv, path_to_tx):
+    "Push local files to remote server"
+    parser = push_parser()
+    (options, args) = parser.parse_args(argv)
+    force_creation = options.force_creation
+    languages = parse_csv_option(options.languages)
+    resources = parse_csv_option(options.resources)
+    skip = options.skip_errors
+    prj = project.Project(path_to_tx)
+    if not (options.push_source or options.push_translations):
+        parser.error("You need to specify at least one of the -s|--source,"
+            " -t|--translations flags with the push command.")
+
+    prj.push(
+        force=force_creation, resources=resources, languages=languages,
+        skip=skip, source=options.push_source,
+        translations=options.push_translations,
+        no_interactive=options.no_interactive
+    )
+    logger.info("Done.")
+
+
+def cmd_pull(argv, path_to_tx):
+    "Pull files from remote server to local repository"
+    parser = pull_parser()
+    (options, args) = parser.parse_args(argv)
+    if options.fetchall and options.languages:
+        parser.error("You can't user a language filter along with the"\
+            " -a|--all option")
+    languages = parse_csv_option(options.languages)
+    resources = parse_csv_option(options.resources)
+    skip = options.skip_errors
+    minimum_perc = options.minimum_perc or None
+
+    try:
+        _go_to_dir(path_to_tx)
+    except UnInitializedError, e:
+        utils.logger.error(e)
+        return
+
+    # instantiate the project.Project
+    prj = project.Project(path_to_tx)
+    prj.pull(
+        languages=languages, resources=resources, overwrite=options.overwrite,
+        fetchall=options.fetchall, fetchsource=options.fetchsource,
+        force=options.force, skip=skip, minimum_perc=minimum_perc,
+        mode=options.mode
+    )
+    logger.info("Done.")
+
+
+def _set_source_file(path_to_tx, resource, lang, path_to_file):
+    """Reusable method to set source file."""
+    proj, res = resource.split('.')
+    if not proj or not res:
+        raise Exception("\"%s.%s\" is not a valid resource identifier. It should"
+            " be in the following format project_slug.resource_slug." %
+            (proj, res))
+    if not lang:
+        raise Exception("You haven't specified a source language.")
+
+    try:
+        _go_to_dir(path_to_tx)
+    except UnInitializedError, e:
+        utils.logger.error(e)
+        return
+
+    if not os.path.exists(path_to_file):
+        raise Exception("tx: File ( %s ) does not exist." %
+            os.path.join(path_to_tx, path_to_file))
+
+    # instantiate the project.Project
+    prj = project.Project(path_to_tx)
+    root_dir = os.path.abspath(path_to_tx)
+
+    if root_dir not in os.path.normpath(os.path.abspath(path_to_file)):
+        raise Exception("File must be under the project root directory.")
+
+    logger.info("Setting source file for resource %s.%s ( %s -> %s )." % (
+        proj, res, lang, path_to_file))
+
+    path_to_file = relpath(path_to_file, root_dir)
+
+    prj = project.Project(path_to_tx)
+
+    # FIXME: Check also if the path to source file already exists.
+    try:
+        try:
+            prj.config.get("%s.%s" % (proj, res), "source_file")
+        except ConfigParser.NoSectionError:
+            prj.config.add_section("%s.%s" % (proj, res))
+        except ConfigParser.NoOptionError:
+            pass
+    finally:
+        prj.config.set("%s.%s" % (proj, res), "source_file",
+           path_to_file)
+        prj.config.set("%s.%s" % (proj, res), "source_lang",
+            lang)
+
+    prj.save()
+
+
+def _set_translation(path_to_tx, resource, lang, path_to_file):
+    """Reusable method to set translation file."""
+
+    proj, res = resource.split('.')
+    if not project or not resource:
+        raise Exception("\"%s\" is not a valid resource identifier. It should"
+            " be in the following format project_slug.resource_slug." %
+            resource)
+
+    try:
+        _go_to_dir(path_to_tx)
+    except UnInitializedError, e:
+        utils.logger.error(e)
+        return
+
+    # Warn the user if the file doesn't exist
+    if not os.path.exists(path_to_file):
+        logger.info("Warning: File '%s' doesn't exist." % path_to_file)
+
+    # instantiate the project.Project
+    prj = project.Project(path_to_tx)
+    root_dir = os.path.abspath(path_to_tx)
+
+    if root_dir not in os.path.normpath(os.path.abspath(path_to_file)):
+        raise Exception("File must be under the project root directory.")
+
+    if lang ==  prj.config.get("%s.%s" % (proj, res), "source_lang"):
+        raise Exception("tx: You cannot set translation file for the source language."
+            " Source languages contain the strings which will be translated!")
+
+    logger.info("Updating translations for resource %s ( %s -> %s )." % (resource,
+        lang, path_to_file))
+    path_to_file = relpath(path_to_file, root_dir)
+    prj.config.set("%s.%s" % (proj, res), "trans.%s" % lang,
+        path_to_file)
+
+    prj.save()
+
+
+def cmd_status(argv, path_to_tx):
+    "Print status of current project"
+    parser = status_parser()
+    (options, args) = parser.parse_args(argv)
+    resources = parse_csv_option(options.resources)
+    prj = project.Project(path_to_tx)
+    resources = prj.get_chosen_resources(resources)
+    resources_num = len(resources)
+    for idx, res in enumerate(resources):
+        p, r = res.split('.')
+        logger.info("%s -> %s (%s of %s)" % (p, r, idx + 1, resources_num))
+        logger.info("Translation Files:")
+        slang = prj.get_resource_option(res, 'source_lang')
+        sfile = prj.get_resource_option(res, 'source_file') or "N/A"
+        lang_map = prj.get_resource_lang_mapping(res)
+        logger.info(" - %s: %s (%s)" % (utils.color_text(slang, "RED"),
+            sfile, utils.color_text("source", "YELLOW")))
+        files = prj.get_resource_files(res)
+        fkeys = files.keys()
+        fkeys.sort()
+        for lang in fkeys:
+            local_lang = lang
+            if lang in lang_map.values():
+                local_lang = lang_map.flip[lang]
+            logger.info(" - %s: %s" % (utils.color_text(local_lang, "RED"),
+                files[lang]))
+        logger.info("")
+
+
+def cmd_help(argv, path_to_tx):
+    """List all available commands"""
+    parser = help_parser()
+    (options, args) = parser.parse_args(argv)
+    if len(args) > 1:
+        parser.error("Multiple arguments received. Exiting...")
+
+    # Get all commands
+    fns = utils.discover_commands()
+
+    # Print help for specific command
+    if len(args) == 1:
+        try:
+            fns[argv[0]](['--help'], path_to_tx)
+        except KeyError:
+            utils.logger.error("Command %s not found" % argv[0])
+    # or print summary of all commands
+
+    # the code below will only be executed if the KeyError exception is thrown
+    # becuase in all other cases the function called with --help will exit
+    # instead of return here
+    keys = fns.keys()
+    keys.sort()
+
+    logger.info("Transifex command line client.\n")
+    logger.info("Available commands are:")
+    for key in keys:
+        logger.info("  %-15s\t%s" % (key, fns[key].func_doc))
+    logger.info("\nFor more information run %s command --help" % sys.argv[0])
+
+
+def cmd_delete(argv, path_to_tx):
+    "Delete an accessible resource or translation in a remote server."
+    parser = delete_parser()
+    (options, args) = parser.parse_args(argv)
+    languages = parse_csv_option(options.languages)
+    resources = parse_csv_option(options.resources)
+    skip = options.skip_errors
+    force = options.force_delete
+    prj = project.Project(path_to_tx)
+    prj.delete(resources, languages, skip, force)
+    logger.info("Done.")
+
+
+def _go_to_dir(path):
+    """Change the current working directory to the directory specified as
+    argument.
+
+    Args:
+        path: The path to chdor to.
+    Raises:
+        UnInitializedError, in case the directory has not been initialized.
+    """
+    if path is None:
+        raise UnInitializedError(
+            "Directory has not been initialzied. "
+            "Did you forget to run 'tx init' first?"
+        )
+    os.chdir(path)
+
+
+def _set_minimum_perc(resource, value, path_to_tx):
+    """Set the minimum percentage in the .tx/config file."""
+    args = (resource, 'minimum_perc', value, path_to_tx, 'set_min_perc')
+    _set_project_option(*args)
+
+
+def _set_mode(resource, value, path_to_tx):
+    """Set the mode in the .tx/config file."""
+    args = (resource, 'mode', value, path_to_tx, 'set_default_mode')
+    _set_project_option(*args)
+
+
+def _set_type(resource, value, path_to_tx):
+    """Set the i18n type in the .tx/config file."""
+    args = (resource, 'type', value, path_to_tx, 'set_i18n_type')
+    _set_project_option(*args)
+
+
+def _set_project_option(resource, name, value, path_to_tx, func_name):
+    """Save the option to the project config file."""
+    if value is None:
+        return
+    if not resource:
+        logger.debug("Setting the %s for all resources." % name)
+        resources = []
+    else:
+        logger.debug("Setting the %s for resource %s." % (name, resource))
+        resources = [resource, ]
+    prj = project.Project(path_to_tx)
+    getattr(prj, func_name)(resources, value)
+    prj.save()